skip to content

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 pageshow

explore

questions

110 · 8 sections

How do `typing.Any` and `object` differ as annotations to a type checker?

level: juniorimportance: must knowfreq 62%
basics
~20 s

Any 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.

open as a page

What does `Optional[str]` mean in a Python type annotation?

level: juniorimportance: must knowfreq 72%
basics
~20 s

Optional[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.

open as a page

Are Python type annotations enforced at runtime, and what does the interpreter do with them?

level: juniorimportance: must knowfreq 75%
basics
~20 s

No. 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.

open as a page

Why is an int accepted where a parameter is annotated float?

level: middleimportance: must knowfreq 58%
basics
~10 s

PEP 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.

open as a page

Why do type checkers reject `def load(path: str = None)` in Python?

level: middleimportance: must knowfreq 60%
basics
~20 s

Because 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.

open as a page

How do `list[int]` and `typing.List[int]` differ in Python 3.14?

level: juniorimportance: must knowfreq 62%
basics
~20 s

Both 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.

open as a page

Why annotate a parameter as Iterable[str] rather than Iterator[str] or list[str]?

level: juniorimportance: must knowfreq 58%
basics
~10 s

collections.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.

open as a page

What does the bracket syntax in `def first[T](xs: list[T]) -> T` declare in Python?

level: juniorimportance: must knowfreq 42%
basics
~20 s

The 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.

open as a page

Why annotate a Python helper with a TypeVar instead of Any?

level: juniorimportance: must knowfreq 70%
basics
~20 s

A 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.

open as a page

How does typing.ParamSpec let a decorator preserve the wrapped function's signature?

level: middleimportance: must knowfreq 48%
basics
~20 s

ParamSpec 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.

open as a page

What does duck typing mean in Python, and when does a missing method actually fail?

level: juniorimportance: must knowfreq 65%
basics
~20 s

Duck 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.

open as a page

How do you declare method and attribute members inside a typing.Protocol body?

level: middleimportance: must knowfreq 50%
basics
~20 s

Write 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.

open as a page

How does typing.Protocol differ from abc.ABC when you define an interface?

level: middleimportance: must knowfreq 50%
basics
~20 s

An 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.

open as a page

Why can isinstance() pass against a runtime_checkable Protocol whose method signature does not match?

level: middleimportance: must knowfreq 58%
basics
~20 s

The 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.

open as a page

Does a class need to inherit from a typing.Protocol for a type checker to accept it?

level: juniorimportance: should knowfreq 35%
basics
~10 s

No. 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.

open as a page

What does typing.TypedDict express that a plain dict[str, str] annotation cannot?

level: juniorimportance: must knowfreq 60%
basics
~20 s

typing.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.

open as a page

What does typing.Literal["read", "write"] express that a plain str annotation cannot?

level: middleimportance: must knowfreq 55%
basics
~20 s

typing.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.

open as a page

What does typing.NewType('VideoId', int) give you that the alias `VideoId = int` does not?

level: middleimportance: must knowfreq 50%
basics
~20 s

typing.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.

open as a page

In Python type hints, why does hard-coding the class name as a fluent method's return type break subclasses?

level: middleimportance: must knowfreq 45%
basics
~20 s

A 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.

open as a page

In a TypedDict, how do total=False, Required and NotRequired differ?

level: middleimportance: must knowfreq 52%
basics
~20 s

total=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.

open as a page

How does an isinstance check narrow a `str | bytes` value for a type checker?

level: juniorimportance: must knowfreq 68%
basics
~20 s

A 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.

open as a page

Why does `if x:` narrow an `int | None` differently from `if x is not None:`?

level: middleimportance: must knowfreq 62%
basics
~20 s

x 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.

open as a page

When is @typing.overload better than annotating one union return type?

level: middleimportance: must knowfreq 45%
basics
~20 s

Use @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.

open as a page

How does `typing.TypeIs` narrowing differ from `typing.TypeGuard` in the else branch?

level: middleimportance: must knowfreq 35%
basics
~20 s

TypeIs 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.

open as a page

In a module using @typing.overload, which def actually runs at call time?

level: juniorimportance: should knowfreq 30%
basics
~20 s

Only 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.

open as a page

How would you stage adding type hints to a large untyped Python codebase?

level: middleimportance: must knowfreq 55%
basics
~20 s

Start 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.

open as a page

What is a `.pyi` stub file in Python, and does it override inline annotations?

level: middleimportance: must knowfreq 45%
basics
~20 s

A .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.

open as a page

How does moving an import under if TYPE_CHECKING: break an import cycle?

level: middleimportance: must knowfreq 52%
basics
~20 s

The 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.

open as a page

What is typing.TYPE_CHECKING, and why guard an import with it?

level: juniorimportance: should knowfreq 42%
basics
~20 s

typing.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.

open as a page

When do you reach for typing.cast() versus a `# type: ignore` comment?

level: middleimportance: should knowfreq 45%
basics
~20 s

Use 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.

open as a page

What is the __annotate__ function that Python 3.14 compiles for annotated objects?

level: middleimportance: must knowfreq 45%
basics
~20 s

It 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.

open as a page

What does typing.get_origin return for str | None versus typing.Union[str, None]?

level: middleimportance: must knowfreq 40%
basics
~20 s

On 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.

open as a page

Why does typing.get_type_hints raise NameError when a webhook receiver resolves its handler annotations at startup?

level: seniorimportance: must knowfreq 55%
basics
~20 s

The 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.

open as a page

Since Python 3.14, do you still need quotes around a forward reference in an annotation?

level: juniorimportance: should knowfreq 35%
basics
~10 s

No. 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.

open as a page

Why does typing.get_type_hints(f) return a class where f.__annotations__ holds a string?

level: juniorimportance: should knowfreq 35%
basics
~20 s

A 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.

open as a page

In Python typing, how does a parameter annotated `type[Digest]` differ from one annotated `Digest`?

level: juniorimportance: must knowfreq 45%
basics
~10 s

type[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.

open as a page

Why does a type checker flag a Python method override that narrows a parameter type?

level: middleimportance: must knowfreq 48%
basics
~20 s

A 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.

open as a page

What does the @override decorator from Python's typing module tell a static type checker?

level: juniorimportance: should knowfreq 38%
basics
~20 s

typing.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.

open as a page

Why does a type checker reject an abstract class passed to a `type[Digest]` parameter?

level: middleimportance: should knowfreq 32%
basics
~20 s

type[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.

open as a page

Why does a type checker reject a subclass that redeclares an inherited attribute with a narrower type?

level: seniorimportance: should knowfreq 28%
basics
~20 s

A 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.

open as a page