Menu

Python Type Hints: Annotations for Functions, Lists, Dicts, and More

What Python type hints are, when they help, and the syntax for annotating variables, function signatures, containers, and optional values.

Annotations That Describe, Don't Enforce

A type hint is a note you attach to a name — usually a function parameter — saying "this should be an int," "this returns a list of strings," and so on. Python doesn't check them at runtime. Passing a string where you annotated an int doesn't raise an error. Your editor and external tools (mypy, pyright, Pyright-via-VS-Code's Pylance) read the hints and warn you before the code runs.

The simplest possible case:

main.py
Output
Click Run to see the output here.

name: str annotates the parameter. -> str annotates the return value. Both calls run. The second one is wrong — a static type checker would flag it — but Python itself happily processes it because 42 happens to support f"{...}" interpolation.

That's the crucial mental model: hints are documentation a machine can read. They don't change the runtime.

Why Bother?

Three concrete wins, in order of how quickly they pay off:

  1. Your editor gets smarter. Autocomplete shows the right methods, renames propagate correctly, and hovering a variable tells you its type.
  2. Function signatures become self-describing. def fetch(url: str, timeout: float = 5.0) -> dict: tells a reader exactly what to pass in and what they'll get back — no need to read the body.
  3. Type checkers catch mistakes before you run the code. Running mypy . on a project surfaces the kind of bugs unit tests often miss — None returned where you expected a value, a dict used where a list belongs.

For a one-file script that's only you and only for today, skip the hints. For anything you'll come back to or share, the fifteen seconds they take to write pay off within the hour.

Basic Built-In Types

You don't need an import for any of these:

main.py
Output
Click Run to see the output here.

Variable annotations (name: str = "Rosa") are rarely necessary — Python infers the type from the right-hand side. Keep them for parameters, return types, and the occasional case where the inferred type is ambiguous.

Functions that return nothing use -> None:

main.py
Output
Click Run to see the output here.

Lists, Dicts, Tuples, and Sets

Containers need a second piece of information — what they contain. Modern Python lets you subscript the built-in types directly:

main.py
Output
Click Run to see the output here.

Reading them out loud:

  • list[float] — a list of floats.
  • dict[str, int] — a dict with string keys and int values.
  • tuple[float, float] — a tuple of exactly two floats.
  • set[str] — a set of strings.

The list[...], dict[...] subscripting syntax works in Python 3.9 and later. In older code you'll see List, Dict, Tuple imported from typing — same meaning, older spelling.

Optional Values

"Might be None" is common. It has two equivalent spellings — both are fine, but the newer one reads better:

main.py
Output
Click Run to see the output here.

str | None means "a string, or None." The | syntax works in Python 3.10+. In older code you'll see Optional[str] from the typing module, which means the same thing.

A caller who sees -> str | None knows to check for None before using the result — that's the whole point of the annotation.

Union Types: This Or That

When a value could be one of several types, use |:

main.py
Output
Click Run to see the output here.

You can union more than two types. int | str | float means "any of these three."

Annotating Variables Inside Functions

Most of the time Python can figure out a local variable's type from its initializer. You only need an annotation when:

main.py
Output
Click Run to see the output here.
  • The container starts empty and the type checker can't guess its contents.
  • The value could be several types and you want to commit to one.
  • You want to document the intent for a human reader.

typing.Any is the escape hatch — "I don't want to annotate this precisely." Use it sparingly. Abuse of Any makes the rest of your type hints worthless.

Annotating Classes

Class attributes and method signatures annotate the same way as any other function:

main.py
Output
Click Run to see the output here.

Dataclasses actually require type annotations — the @dataclass decorator reads them to generate __init__ and __repr__. That's the one place annotations do affect runtime behavior.

Tuples and the "Any Length" Case

tuple[...] has two shapes that confuse newcomers:

main.py
Output
Click Run to see the output here.
  • tuple[float, float] — exactly two floats.
  • tuple[int, ...] — any number of ints. The ... (a real syntax element in the type system) means "and so on."

Callables and Type Aliases

When a function takes or returns another function, use Callable:

main.py
Output
Click Run to see the output here.

Callable[[int], int] means "a function that takes one int and returns an int."

When an annotation gets repetitive, name it:

main.py
Output
Click Run to see the output here.

An alias is just a regular Python assignment. Anywhere you'd use the long form, the short name works.

Running a Type Checker

Python's own interpreter ignores type hints. To actually check them, install a type checker. mypy is the original; pyright (used by VS Code's Pylance) is faster.

pip install mypy
mypy your_project/

The first run will surface errors in places you hadn't realised. Work through them incrementally — # type: ignore silences a single line when you need to move on.

Modern IDEs run type checking continuously as you edit, so most feedback arrives before you save.

When Type Hints Don't Fit

  • Quick exploratory scripts. Annotations add friction to code that lives for an hour.
  • Heavily dynamic code. Metaprogramming, plugin systems, and similar patterns often outgrow what the type system can describe. Annotate the outer API and let the internals stay loose.
  • Third-party libraries without types. If a library you import has no type info, Any leaks into your code. Fine — it's not your code to annotate.

For everything in between, type hints are a small habit with a big payoff. The cost is a few extra keystrokes per function signature. The return is fewer bugs, easier refactors, and code that documents itself.

Next: Modules and Imports

You've now got all the function-level tooling — arguments, decorators, type hints. Next we'll look at how Python organises code across files: modules, packages, and the import system.

Frequently Asked Questions

What are type hints in Python?

Type hints are annotations that describe the expected types of variables, function parameters, and return values. Python itself doesn't enforce them at runtime — they're for tools (IDEs, linters, type checkers like mypy or pyright) and for humans reading the code.

Do type hints make Python run faster?

No. The Python interpreter ignores type hints at runtime. The speedup is in your development loop — fewer typos caught by editors, clearer function signatures, safer refactors.

When should I add type hints?

Add them to public function signatures — parameters and return types. Add them sparingly inside function bodies, only where a variable's type isn't obvious. For one-off scripts, they're optional. For shared code and libraries, they pay for themselves quickly.

Learn to code with Coddy

GET STARTED