Python remains dynamically typed at runtime, but since Python 3.5, type hints let you optionally annotate variables and functions with the types you intend — purely for documentation and tooling, with zero effect on how the code actually runs.
age: int = 25
name: str = "Rahim"
price: float = 19.99
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * times
# Type hints are NOT enforced at runtime:
age = "not a number" # runs perfectly fine, Python ignores the hint entirely
The colon-based syntax (age: int) and the arrow (-> str) declare intended types, checked by external tools like mypy or pyright, and understood by editors for autocomplete — but the Python interpreter itself never checks or enforces them at runtime, which is why the reassignment above doesn't raise an error despite technically violating the hint.
Type hints have become the norm in professional, larger Python codebases because they catch a huge class of bugs before code even runs (via a type checker in CI) and make function signatures self-documenting — def process(data: list[dict[str, int]]) -> None: tells a reader exactly what shape of data is expected, without needing to read the function body.
Modern Python (3.9+) allows built-in collection types to be used directly as generics (list[int], dict[str, int]) rather than the older typing.List[int]/typing.Dict[str, int] forms — always prefer the modern lowercase syntax in new code.