Argparse, type hint

What should be the type hint when I pass an argparse argument to a function in Python?

What do you mean by “an argparse argument”?

If you mean “the thing that I got back from a parse_args call”, its type is argparse.Namespace; although really this class does not do anything meaningful (it’s just there to store attributes)… there probably isn’t any reason to restrict the type beyond object (or Any).

If you mean a specific one of the arguments that was parsed (and stored as an attribute in that Namespace), then the type should be whatever type you told argparse to parse for that argument.

If you mean that you want the function to expect that its argument has certain specific attributes, see here:

If you mean something else, please be clearer about it.

1 Like

How is it that object or Any is better than argparse.Namespace?

Because it doesn’t try to prevent you from passing things that work just as well, when there isn’t a logical reason to prevent it.

1 Like

Does it REALLY have to be an argparse.Namespace? For example, I could write this function:

def main(args):
    if args.verbose: print("Starting")
    count = 0
    for thing in args.things:
        if args.verbose: print("Thinking", thing)
        if not args.dryrun: think(thing)
        count += 1
        if args.progress: progress_bar(count, len(things), args.progress)
    if args.verbose: print("Done thinking!")

Does this function require that its parameter be an argparse.Namespace? Sure you can pass it one, but that’s not actually what it demands - it just demands some object with these attributes.

This is a problem with a lot of type hints: someone gets the feeling that “everything NEEDS type annotations” and then just writes in whatever the current code is feeding to the function. That’s actually not very useful - a static analyzer could already figure that out - and only serves to restrict future usage. For the function I gave above, it should be absolutely fine to pass it a dataclass like this:

@dataclass
class Config:
    verbose: bool
    dryrun: bool
    progress: str | None
    things: Sequence[str]

which would be a quite handy way to define the settings if this were being imported into another module - and it requires zero changes to the existing code, unless it’s been overly-restrictively-typed.

1 Like

So I should pretty much only care about str, int, bool etc. and leave everything else alone?

You make me feel like type hints is useless (I probably just don’t know how to use it properly).

That wasn’t my intention, sorry. I would say that for this specific argument, they are probably useless, but that’s more because it’s not easy to adequately hint the specific requirements (“needs to have all of these attributes” gets very clunky).

The line of argument would be completely different if, instead of passing the argparse object as a whole, it was passed as keyword args. Consider:

de process(...): # fill this in later
    ... mostly the same code shown above

def main(args):
    process(**vars(args))

How would you define the arguments to this new process() function? Each one is a separate attribute, which means you could give them their separate names, type hints, defaults, and anything else you want to say about them. So in that instance, type hinting would be a lot easier to do usefully.

In terms of your other comment:

I would say that the most interesting type hints would be the difference between scalar values and collections, plus anything where you need particular features. There’s a vast difference between Sequence[int] and int, and if you’re expecting a datetime.datetime or a file, you can annotate that.

But the best part of gradual typing is that you don’t need to care about ANY of it if you don’t want to. Just write your code without type hints, and then see how much the type checker can figure out from usage! There’s a good chance that you can get excellent “bang for your buck” by type-annotating just a few key places (such as externally-called entry points), and letting the rest be deduced automatically.

(This is one of the things I find frustrating in TypeScript, incidentally. Once you switch from JavaScript to TypeScript, suddenly you have to satisfy the compiler all over the place, even with things that it really seems like it should be able to figure out.)

2 Likes

Well done on your argparse journey :slight_smile: Yeah type hints are optional in Python (thankfully). Great to see you trying them out! Sometimes they are neat to have around, but you can live an equally happy life without them in CLI. :slight_smile: Gradually as you learn more about them, you can add them in, but they certainly aren’t required.

If you still want to type annotate, give us a minimal example and we can help further.

2 Likes

Don’t be sorry, I don’t mean that in a negative way, I just don’t understand what the purpose of type hints really is.

My knowledge of English and programming is not helping me understand what you are trying to teach me. But you seem to be unpacking the args before passing them to the function.

Thanks! at first I thought of type hints as helpful to make my script more secure, but I’m wrong and now I use them to make my code more explicit (I think). When I used argparse.Namespace, mypy didn’t detect an error in the function, unlike when I just didn’t use type hints.

Ah, cool cool.

It’s a tool that can help you discover bugs. As a general rule, debugging is the art of removing bugs from code, and programming is the art of putting new bugs into your code. To be productive as programmers, we need to add bugs and then remove them, as quickly as possible. I’m being a little facetious, but it’s true - programming is not about being perfect before doing anything, it’s about getting something done and then improving it. We’re like Bob Ross making paintings - you do something, then you adjust it, and you make happy accidents now and then.

Once you’ve made a bug, there are lots of times when you might discover it:

  1. Right as you’re typing it in, because your editor underlines it in red
  2. The moment you save, when a script checks your code.
  3. When you first run the program and see what happens.
  4. When you run your test suite (you do have one, right?)
  5. When you show your code to someone else.
  6. When you merge into trunk and let lots of people test it.
  7. When you run it on your staging server, or release an alpha or beta.
  8. When it hits production.
  9. After it’s been in production for years.
  10. After you make a supposedly-unrelated change a decade later and suddenly everything breaks.

Your goal, as a programmer, is to find those problems SOONER. How important this is depends on how much impact it would have if the problem was found a bit later. For example, a bug that slips past you and gets caught in code review is a minor problem (you lose a bit of time for both you and the other person), so it’s better to catch it on your own first, but you haven’t broken anything. On the other hand, a software bug that gets you all the way to the moon and then attempts to land your vehicle some distance above the surface is a bit more of a problem (and a tragedy for the lunar explorer itself).

So where does type hinting come in? Somewhere around the level of unit tests, but without the need to write a ton of tests. And depending on how your tooling is set up, it can even be done the instant you save your code. Here’s an example:

def bind(port: Number):
    ...

def configure():
    bind(os.environ.get("PORT", 12345))

If the environment variable isn’t set, this will work just fine. But if it is, you now have a string coming in. A type checker can tell you immediately that this is a bad idea, since os.environ is defined as carrying strings. (Or at least, it would be if I had the appropriate stub files installed - I tested this on my system and got no complaints. So this might be a bad example.)

Broadly speaking, the goal of type hinting is to spend less time adding the hints than you save by catching bugs sooner. That’s why you’re not forced to go through all your code and add hints to every variable everywhere; the vast majority of them are unexciting and pretty obvious, so the type checker can figure them out just fine. What you mostly want to do is:

  • Type-hint what isn’t obvious. For example, n: int | None = 5 gives more information than just n = 5 as it tells you that this might be None later on. Not always necessary but the option’s there when you need it.
  • Add hints to key parameters in externally-callable functions. This serves as machine-readable documentation and allows other projects to benefit, so the cost is on you (and fairly minimal if you were going to document valid types anyway), and the benefit is for everyone, which lets the benefit scale quite nicely.
  • Or, just do nothing. Type hinting isn’t of value to everyone (or for every project), so it is perfectly valid to decide not to add hints to your code. It’s an effort tradeoff.

Does running mypy help you? Would it help you more if you added hints to some key variables/parameters? Then add them! Does one particular parameter really not gain much by having a type hint? Then don’t have one, or stick an Any on there or something. No pressure either way.

Like UnitTest? If so, I don’t know how to do unit testing because I don’t see the point of using it because I use a bunch of linters and test my code manually.

By the way, thank you for your patience and kindness. You helped me make the final review of my code a reality.

Yes, UnitTest is one framework for writing tests. We programmers are really good at having the tools to make test suites, and then… not making test suites. I’m very guilty of this myself.

Linters are a slightly different beast, they serve the same purpose of “find bugs sooner” but tend to find a different class of bug. Unit testing (and automated testing in general) is intended as a way to, well, automate that manual testing. Imagine the testing you currently do; now imagine writing down notes and saying “every time I make any significant change, I need to test this, this, this, this, and this”; and now imagine getting a computer to do that testing for you, since it’s now a repetitive task. THAT is the job of your test suite. Unit testing in particular is a form of that where, instead of testing the entire app as a whole, you test one small part (“unit”) at a time, usually by putting it in a special environment where it can’t see the rest of the program. Can be extremely helpful, depending on how things are designed, but it’s also a lot of work to go back and add it later.

No problem! I’m always happy to help people who are genuinely interested in learning. Keep on being curious and willing to learn! Worked for Alice in Wonderland, works for us too.

1 Like

Right. mypy and type hints go hand-in-hand, but you don’t need to run mypy or use type hints. Those are relatively new things in the Python world, so it’s optional. It doesn’t make code more secure. It’s just a nice thing (sometimes) usually with big code bases. Frankly if you’re new to Python, I’d ignore them completely. You can play with them later :smiley:

Regarding unit tests, you can skip straight to using pytest. Don’t even worry about the unittest module. Unit tests are great to have. Testing CLIs is a little tricky, so refer to some links I’ve posted in the past on that.

1 Like

Side note: MyPy isn’t the only type checker out there, and it’s deliberately NOT part of the Python standard library. You should be able to use the exact same type hints with any checker, since the hints themselves are standardized, but the checkers can have different rules and different handling of odd cases.

But yes, they’re all optional, and importantly, “Python-with-type-hints” is still perfectly normal Python code. You can have annotations in your code and run it without any changes.

1 Like