{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/python/typeguard-vs-typeis/",
  "description": "Understand TypeIs vs TypeGuard in Python: TypeIs provides more intuitive type narrowing by narrowing both positive and negative branches.",
  "path": "/python/typeguard-vs-typeis/",
  "publishedAt": "2024-04-27T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Python",
    "Typing",
    "TIL"
  ],
  "textContent": "The handful of times I've reached for typing.TypeGuard in Python, I've always been\nconfused by its behavior and ended up ditching it with a type: ignore comment.\n\nFor the uninitiated, TypeGuard allows you to apply custom [type narrowing]. For example,\nlet's say you have a function named pretty_print that accepts a few different types and\nprints them differently onto the console:\n\nIf you run it through mypy, in each branch, the type checker automatically narrows the\ntype and knows exactly what the type of val is. You can test the narrowed type in each\nbranch with the typing.assert_type function.\n\nThis works well for 99% of cases, but occasionally, you need to check an incoming value more\nthoroughly to determine its type and want to take action based on the narrowed type. In\nthose cases, just using isinstance may not be sufficient. So, you need to factor out the\ncomplex type checking logic into a separate function and return a boolean depending on\nwhether the inbound value satisfies all the criteria to be of the expected type. For\nexample:\n\nHere, is_person first checks that the inbound dictionary conforms to the structure of the\nPerson typeddict and then verifies that name is at least 1 character long and age is\nbetween 0 and 150.\n\nThis is a bit more involved than just checking the type with isinstance and the type\nchecker needs a little more help from the user. Although the return type of the is_person\nfunction is bool, typing it with TypeGuard[Person] signals the type checker that if the\ninbound value satisfies all the constraints and the checker function returns True, the\nunderlying type of val is Person.\n\nYou can see more examples of TypeGuard in [PEP 647].\n\nAll good. However, I find the behavior of TypeGuard a bit unintuitive whenever I need to\ncouple it with union types. For example:\n\nIn the if branch, TypeGuard signals the type checker correctly that the narrowed type of\nthe inbound value is int | float but in the else branch, I was expecting it to be str\nbecause the truthy if condition has already filtered out the int | float. But instead,\nwe get str | int | float as the narrowed type. While there might be a valid reason behind\nthis design choice, the resulting behavior with union types made TypeGuard fairly useless\nfor cases I wanted to use it for.\n\nTypeIs has been introduced via [PEP 742] to fix exactly that. The PEP agrees that people\nmight find the current behavior of TypeGuard a bit unexpected and introducing another\nconstruct with a slightly different behavior doesn't make things any less confusing.\n\n> We acknowledge that this leads to an unfortunate situation where there are two constructs\n> with a similar purpose and similar semantics. We believe that users are more likely to\n> want the behavior of TypeIs, the new form proposed in this PEP, and therefore we\n> recommend that documentation emphasize TypeIs over TypeGuard as a more commonly\n> applicable tool.\n\nTypeGuard and TypeIs have similar semantics, except, the latter can narrow the type in\nboth the if and else branches of a conditional. Here's another example with a union type\nwhere TypeIs does what I expected TypeGuard to do:\n\nNotice that TypeIs has now correctly narrowed the type in the else branch as well. This\nwould've also worked had we returned early from the pretty_print function in the if\nbranch and skipped the else branch altogether. Exactly what I need!\n\nHere are a few [typeshed stubs] for the stdlib functions in the inspect module that are\nalready taking advantage of the new TypeIs construct:\n\n\n\n\n[type narrowing]:\n    https://mypy.readthedocs.io/en/latest/type_narrowing.html\n\n[pep 647]:\n    https://peps.python.org/pep-0647/\n\n[pep 742]:\n    https://peps.python.org/pep-0742/\n\n[typeshed stubs]:\n    https://github.com/python/typeshed/blob/bdf75023dfe3930deac1c6b4e269770427b106c4/stdlib/inspect.pyi",
  "title": "TypeIs does what I thought TypeGuard would do in Python"
}