Type Hints and Static Typing
Python's gradual type system: the annotations you write and what a checker makes of them — unions, generics, Protocols, TypedDict, narrowing. It is what keeps a big codebase safe to change.
part ofPythonoverview, primer and where to startread it →on this pageshowhide
explore
- Writing Annotations15 questions
- Annotation Syntax and Forward References4 questions
- Optional, Unions, and None4 questions
- Any, object, and the Bottom Types3 questions
- Numeric Tower Duck Types4 questions
- Generics and Variance24 questions
- TypeVar and Generic Classes4 questions
- Builtin and ABC Generics4 questions
- Covariance and Contravariance4 questions
- Callables and ParamSpec4 questions
- PEP 695 Type Parameters4 questions
- Iterables, Generators, Awaitables4 questions
- Structural Typing and Protocols12 questions
- typing.Protocol4 questions
- Protocol vs ABC and Duck Typing4 questions
- isinstance with runtime_checkable4 questions
- Typed Data Shapes16 questions
- TypedDict4 questions
- Literal, Final, and ClassVar4 questions
- NewType, Type Aliases, and Annotated4 questions
- Self and Fluent APIs4 questions
- Narrowing and Overloads12 questions
- isinstance Narrowing and Exhaustiveness4 questions
- TypeGuard and TypeIs4 questions
- @overload Signatures4 questions
- Gradual Typing in Practice12 questions
- Stub Files and py.typed4 questions
- Adopting Types in Legacy Code4 questions
- TYPE_CHECKING and Version Skew4 questions
- Annotations at Runtime12 questions
- PEP 649 Deferred Evaluation4 questions
- get_type_hints Resolution4 questions
- get_origin and get_args4 questions
- Class Objects and Overrides7 questions
- type[C] and Class Objects4 questions
- @override and Liskov Checks3 questions
questions
110 · 8 sectionsHow do `typing.Any` and `object` differ as annotations to a type checker?
basics
~20 sAny switches checking off for a value: any attribute, call or assignment is allowed. object is the top type, so every value fits it, but you may use only what every object supports until you narrow with isinstance.
What does `Optional[str]` mean in a Python type annotation?
basics
~20 sOptional[str] from the typing module means the value is either a str or None - exactly the union str | None. It says nothing about omitting an argument: such a parameter is still required unless it also has a default.
Are Python type annotations enforced at runtime, and what does the interpreter do with them?
basics
~20 sNo. CPython never compares a value against its annotation. It only records annotations as metadata in the annotations dictionary on functions, classes and modules; enforcement comes from a separate static type checker or from library code that reads them.
Why is an int accepted where a parameter is annotated float?
basics
~10 sPEP 484 special-cases the numeric tower: an int is acceptable where float is annotated, and both where complex is. It is a type-checker convention, not subclassing, and nothing is converted at runtime.
Why do type checkers reject `def load(path: str = None)` in Python?
basics
~20 sBecause the annotation says path is always a str while the default hands it None. Under the no-implicit-Optional rule a None default no longer widens the declared type, so you must write str | None = None yourself.
How do `list[int]` and `typing.List[int]` differ in Python 3.14?
basics
~20 sBoth say 'a list of ints' to a type checker. Since PEP 585 in Python 3.9 the builtin list is subscriptable itself, so list[int] is the current spelling and typing.List is a deprecated alias kept only for older code.
Why annotate a parameter as Iterable[str] rather than Iterator[str] or list[str]?
basics
~10 scollections.abc.Iterable[str] says only that the function will loop over the argument, so lists, sets, dict views and generators all fit. Iterator[str] demands a one-shot cursor, and list[str] rejects every other container.
What does the bracket syntax in `def first[T](xs: list[T]) -> T` declare in Python?
basics
~20 sThe brackets declare a type parameter T belonging to that function, tying the argument's element type to the return type. Python 3.12 added the syntax in PEP 695; before it you declared T with a module-level TypeVar call.
Why annotate a Python helper with a TypeVar instead of Any?
basics
~20 sA TypeVar is a placeholder that ties the argument type to the return type, so a checker knows a helper called with a list of strings gives back a string. Any severs that link and switches checking off for everything downstream.
How does typing.ParamSpec let a decorator preserve the wrapped function's signature?
basics
~20 sParamSpec is a type variable that captures a whole parameter list. Typing a decorator as Callable[P, R] to Callable[P, R], with the inner wrapper declared *args: P.args, **kwargs: P.kwargs, hands callers back the original signature instead of erasing it.
What does duck typing mean in Python, and when does a missing method actually fail?
basics
~20 sDuck typing means Python cares about the attributes an object actually has, not the class it inherits from. Nothing is verified when you pass the object; the AttributeError arrives only at the moment the missing method is looked up and called.
How do you declare method and attribute members inside a typing.Protocol body?
basics
~20 sWrite methods as signatures with an ellipsis body, and data members as bare annotations. A plain annotation means a read-write instance attribute; declare a read-only member with a property in the protocol body, and a class-level constant with ClassVar.
How does typing.Protocol differ from abc.ABC when you define an interface?
basics
~20 sAn abc.ABC is nominal: a class conforms only by inheriting from it, and the interpreter enforces that at instantiation. A typing.Protocol is structural: any class with matching members conforms, checked by a static type checker with no inheritance and no runtime cost.
Why can isinstance() pass against a runtime_checkable Protocol whose method signature does not match?
basics
~20 sThe runtime check is presence-only: it asks whether the object has an attribute for each member name the protocol declares, and stops there. Parameter lists, argument counts, return types and attribute types are never compared, so a wrongly-shaped object passes.
Does a class need to inherit from a typing.Protocol for a type checker to accept it?
basics
~10 sNo. typing.Protocol is structural: a static type checker accepts any class whose members match the protocol's declared methods and attributes, with no base class, no registration and no import on the implementing side.
What does typing.TypedDict express that a plain dict[str, str] annotation cannot?
basics
~20 styping.TypedDict names each key of a dictionary and gives every key its own value type, so a type checker can flag a misspelled, missing or undeclared key. dict[str, str] only says string keys, string values.
What does typing.Literal["read", "write"] express that a plain str annotation cannot?
basics
~20 styping.Literal pins a value down to an exact set of allowed constants, so a checker accepts only "read" or "write" where a str annotation would accept any string. It constrains nothing at runtime; CPython still passes any string through.
What does typing.NewType('VideoId', int) give you that the alias `VideoId = int` does not?
basics
~20 styping.NewType creates a type a checker keeps distinct from its base: a plain int is rejected where a VideoId is expected. The alias VideoId = int is transparent and stops nothing. At runtime the call returns its argument unchanged.
In Python type hints, why does hard-coding the class name as a fluent method's return type break subclasses?
basics
~20 sA checker takes the annotation literally, so the method is treated as returning the base class and the subclass's own methods disappear from the rest of the chain. typing.Self instead resolves to whichever class the call was made on.
In a TypedDict, how do total=False, Required and NotRequired differ?
basics
~20 stotal=False is a per-class default that makes every key declared in that class body optional. Required and NotRequired are per-key modifiers that override the class default on one key, so a single class can mix both.
How does an isinstance check narrow a `str | bytes` value for a type checker?
basics
~20 sA static type checker follows control flow: inside if isinstance(chunk, bytes): the declared union str | bytes shrinks to bytes, and the other branch keeps only str. The narrowing lasts until the name is rebound.
Why does `if x:` narrow an `int | None` differently from `if x is not None:`?
basics
~20 sx is not None splits the union exactly: int in the true branch, None in the false one. if x: only proves truthiness, so 0 takes the false branch and the checker still types that branch int | None.
When is @typing.overload better than annotating one union return type?
basics
~20 sUse @typing.overload when the return type depends on which arguments were passed. A single union return makes every caller narrow it; overloads give each call site the one exact type, and can make invalid argument combinations a checker error.
How does `typing.TypeIs` narrowing differ from `typing.TypeGuard` in the else branch?
basics
~20 sTypeIs narrows both branches: the value is the guarded type in the if branch and has that type subtracted in the else branch. TypeGuard narrows only the if branch and leaves the else branch at the declared type.
In a module using @typing.overload, which def actually runs at call time?
basics
~20 sOnly the final, undecorated def has a real body and runs. The @typing.overload stubs above it exist for the type checker, their ellipsis bodies are never executed, and a name that has only stubs raises NotImplementedError when called.
How would you stage adding type hints to a large untyped Python codebase?
basics
~20 sStart at the public boundary: annotate the function and method signatures other modules import, then work inward. Type one module per change, keep annotation-only diffs separate from behaviour changes, and let the checker infer locals.
What is a `.pyi` stub file in Python, and does it override inline annotations?
basics
~20 sA .pyi stub is a Python-syntax file carrying only signatures, with ... for every body. A static type checker reads the stub instead of the matching .py module, so it fully overrides that module's inline annotations.
How does moving an import under if TYPE_CHECKING: break an import cycle?
basics
~20 sThe cycle exists only because both modules import each other while running. If one direction is needed purely for annotations, guarding it with TYPE_CHECKING deletes that runtime edge, and the checker still sees both modules, so the hints keep resolving.
What is typing.TYPE_CHECKING, and why guard an import with it?
basics
~20 styping.TYPE_CHECKING is a constant that is False whenever the program actually runs, but every static type checker analyses the block as if it were True. An import placed inside that block therefore exists only for the checker.
When do you reach for typing.cast() versus a `# type: ignore` comment?
basics
~20 sUse typing.cast when you know the value's real type and the checker cannot see it; it returns the value unchanged at runtime. Use a suppression comment, with its error code, only when the checker itself is wrong.
What is the __annotate__ function that Python 3.14 compiles for annotated objects?
basics
~20 sIt is an implicitly compiled function holding a function's, class's or module's annotation expressions. Python 3.14 attaches it as annotate and calls it with a format argument the first time annotations is read, then caches the result.
What does typing.get_origin return for str | None versus typing.Union[str, None]?
basics
~20 sOn Python 3.14 both return typing.Union, because typing.Union and types.UnionType are now the same object. typing.get_args gives (str, NoneType) for either spelling. On 3.10 through 3.13 the pipe form reported types.UnionType and the bracket form reported typing.Union.
Why does typing.get_type_hints raise NameError when a webhook receiver resolves its handler annotations at startup?
basics
~20 sThe annotation names a class imported only inside an if TYPE_CHECKING block. That constant is False at runtime, so the name never enters the defining module's globals and resolution fails. Import it for real, or pass it in via localns.
Since Python 3.14, do you still need quotes around a forward reference in an annotation?
basics
~10 sNo. Python 3.14 evaluates annotations lazily, so a name used in an annotation only has to exist when something reads the annotations, not when the def or class statement runs. Quotes became optional.
Why does typing.get_type_hints(f) return a class where f.__annotations__ holds a string?
basics
~20 sA quoted annotation such as "Order" is stored verbatim as a str. typing.get_type_hints evaluates that text as an expression in the function's module globals and returns a new dict whose values are the real type objects.
In Python typing, how does a parameter annotated `type[Digest]` differ from one annotated `Digest`?
basics
~10 stype[Digest] accepts the class object itself, Digest or any subclass, so the function can call it to build instances. A bare Digest annotation accepts an already-built instance instead.
Why does a type checker flag a Python method override that narrows a parameter type?
basics
~20 sA caller holding a base-typed reference may pass anything the base's signature accepts, and the subclass instance must handle it. So an override may widen a parameter type but never narrow it; return types go the other way and may only narrow.
What does the @override decorator from Python's typing module tell a static type checker?
basics
~20 styping.override, added in Python 3.12 by PEP 698, marks a method as deliberately replacing one inherited from a base class. A checker reports an error when no base class declares that name. At run time the decorator does nothing.
Why does a type checker reject an abstract class passed to a `type[Digest]` parameter?
basics
~20 stype[Digest] promises the callee may call the class to build an instance. An abstract base cannot be called — doing so raises TypeError — so checkers refuse it at the call site rather than let the failure reach runtime.
Why does a type checker reject a subclass that redeclares an inherited attribute with a narrower type?
basics
~20 sA plain attribute is both read and written through a base-typed reference, so its declared type is invariant: base code may assign anything the base's annotation allows, which a narrower subclass declaration cannot honour. Only read-only positions may narrow.