Course topics

By WebNest Studio Engineering Team

Python Tutorial

Type Hints

Python is, and remains, a dynamically typed language: variable types are determined at runtime, and nothing stops you from assigning a string to a variable that previously held an integer. Type hints, introduced by PEP 484, add an optional layer of static type annotations on top of this — a way to document, and have tools verify, what type a variable, parameter, or return value is intended to hold, without changing how the interpreter actually runs your code.

This lesson covers the syntax for annotating variables, function parameters, and return types; generic annotations like Optional, Union, and the modern list[int] style; and how a static checker such as mypy uses these hints to catch type errors before the program ever runs.

Why it matters

Type hints state what a function expects and returns, and tools check them before the code runs. They catch a whole class of mistakes while you type, power the autocompletion in your editor, and are required by FastAPI and Pydantic, which read them to validate data. Most professional Python code written today is annotated.

Annotating Variables, Parameters, and Return Types

A variable annotation follows the name with a colon and a type: age: int = 25. A function parameter is annotated the same way inside the parentheses, and the return type is written after an arrow following the closing parenthesis: def greet(name: str) -> str:. None of this is enforced by the Python interpreter at runtime — it is purely informational and is used by editors for autocompletion and by external static type checkers, not by the language itself while the program is executing.

Optional, Union, and Generic Collection Types

Many values can legitimately be one type or another, or can be absent entirely, and type hints have specific syntax for this. Optional[int] means "an int, or None." Union[int, str] means "either an int or a str." Since Python 3.10, the | operator can be used instead: int | None is equivalent to Optional[int]. For collections, modern Python (3.9+) allows built-in generics directly: list[int] means "a list containing ints," dict[str, float] means "a dict mapping strings to floats" — no need to import List or Dict from the older typing module for these common cases.

  • Optional[str] (or str | None) — a string, or the absence of a value.
  • Union[int, str] (or int | str) — a value that could be either type.
  • list[int], dict[str, int], tuple[int, int] — built-in generic collection annotations.

Catching Errors with a Static Checker (mypy)

Type hints only become genuinely useful for catching bugs when a tool actually reads and checks them, since Python itself ignores them at runtime. mypy is the most widely used static type checker: run mypy myscript.py from the terminal, and it analyzes your annotated code, flagging places where a function is called with the wrong argument type, where a variable's declared type conflicts with the value assigned to it, or where a return value doesn't match the declared return type — all without running the program at all.

Python Remains Dynamically Typed at Runtime

It is important to be precise about what type hints do not do: they are not enforced automatically by the Python interpreter. You can annotate a parameter as x: int and then call the function with a string, and the program will run without any TypeError at the point of the call — the mismatch would only be caught if you separately ran a checker like mypy against the code beforehand. Type hints are a documentation and tooling layer, not a runtime type system, which is a common point of confusion for developers coming from statically typed languages like Java or TypeScript's compiled output.

Example

Annotated function with Optional and list generics, checked with mypy

Python
from typing import Optional

def find_average(scores: list[int]) -> Optional[float]:
    if not scores:
        return None
    return sum(scores) / len(scores)

result = find_average([85, 90, 78])
print(result)

# This call passes a str where a list[int] is expected.
# Python runs it anyway (hints aren't enforced at runtime),
# but "mypy this_file.py" would flag it before you ever run it.
bad_result = find_average("not a list")
Output
84.33333333333333
# mypy output (static check, not a runtime error):
error: Argument 1 to "find_average" has incompatible type "str"; expected "list[int]"

Common Mistakes

  • Believing type hints prevent type-mismatch bugs automatically — they only help if a checker like mypy is actually run, typically in CI or as a pre-commit step.
  • Importing List, Dict, and Optional from typing for simple cases in modern Python, when built-in generics like list[int] and the X | None syntax are simpler and preferred since Python 3.9/3.10.
  • Forgetting that a function without a return type annotation is treated as returning "Any" by most checkers, silently skipping useful checks.
  • Adding overly complex nested generic types where a simpler annotation (or a dataclass) would communicate intent more clearly.

Exercise

Add type hints to both functions. find_user takes a dictionary from int to str and an int, and returns a str or None. double takes and returns an int. The last line shows that Python itself does not enforce the hints.

Expected output: Asha, None, abab (one per line)

Python · starter code
# TODO: add type hints to the parameters and the return value
def find_user(users, user_id):
    return users.get(user_id)


# TODO: add type hints
def double(value):
    return value * 2


print(find_user({1: "Asha"}, 1))
print(find_user({1: "Asha"}, 2))
print(double("ab"))  # a type checker would report this call; Python runs it
Show hints
  • A dictionary type is written dict[int, str], and "str or None" is written str | None.
  • The return type follows an arrow after the parameter list: def f(x: int) -> int:.
Show solution
Python · solution
def find_user(users: dict[int, str], user_id: int) -> str | None:
    return users.get(user_id)


def double(value: int) -> int:
    return value * 2


print(find_user({1: "Asha"}, 1))
print(find_user({1: "Asha"}, 2))
print(double("ab"))  # a type checker would report this call; Python runs it

Quick quiz

1. Does Python check type hints when the program runs?

  • A. Yes, and it raises TypeError
  • B. Only in strict mode
  • C. Only for function parameters
  • D. No; they are checked by separate tools such as mypy
Show answer

D. The interpreter stores the hints but does not enforce them.

2. What does Optional[str] mean?

  • A. The value is a str or None
  • B. The parameter can be omitted
  • C. The value can be of any type
  • D. The string may be empty
Show answer

A. It is the same as str | None. It says nothing about whether an argument has a default.

3. How is "a list of integers" written as a type hint in current Python?

  • A. list(int)
  • B. list[int]
  • C. List<int>
  • D. int[]
Show answer

B. Built-in collection types have accepted square brackets since Python 3.9.

Interview questions

What are the benefits of type hints?

Show answer

They document what a function takes and returns in a form that tools can verify. A type checker such as mypy or Pyright finds mismatched types, missing None checks and misspelled attributes without running the code. Editors use them for autocompletion and safe refactoring, and libraries such as Pydantic and FastAPI use them at runtime to validate and convert data.

Are type hints enforced at runtime?

Show answer

No. Python stores them in __annotations__ and otherwise ignores them, so passing a string where an int is annotated raises no error by itself. Enforcement comes from running a static checker, or from libraries that read the annotations and validate values, as Pydantic does. Python remains dynamically typed.

Key Points to Remember

  • Type hints (PEP 484) let you annotate variables, parameters, and return types, but Python remains dynamically typed and does not enforce them at runtime.
  • Optional[X] means X or None; Union[X, Y] (or X | Y since 3.10) means either type; list[int]/dict[str, int] are built-in generic collection annotations.
  • A static checker like mypy reads these annotations and reports type errors before the code ever runs, which is where the real value of type hints comes from.
  • Hints are a documentation and tooling aid, not a substitute for runtime validation when it truly matters (e.g. validating external input).

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs Python in your browser. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering