Quick Answer

Type hints annotate what a function expects and returns. Python does not enforce them at all — passing the wrong type still runs. Their value comes from editors and from a checker like mypy, which reads them and reports mismatches before you run anything.

They are not enforced. At all.

This is the thing to internalise first, because most confusion comes from assuming otherwise.

def greet(name: str, times: int = 1) -> str:
    return f"Hi {name}! " * times

print(greet("Asha", 2))
# Hi Asha! Hi Asha!

print(greet(42, 1))
# Hi 42!            <-- an int, no error whatsoever

Passing an int where str was declared runs perfectly. Python stores the annotations and otherwise ignores them:

print(greet.__annotations__)
# {'name': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}

They are metadata. If you need runtime validation — for API input, for example — you need an actual validation library; hints alone will not do it. This mirrors TypeScript, where types are erased before the code runs.

The syntax worth knowing

from typing import Optional, Union

def f(a: int, b: str = "x") -> bool: ...

names: list[str] = []
scores: dict[str, int] = {}
pair: tuple[int, str] = (1, "a")

def find(id: int) -> Optional[str]: ...     # str or None
def parse(v: Union[int, str]) -> int: ...   # either

Since Python 3.9 you can write list[str] and dict[str, int] directly rather than importing List and Dict. Since 3.10, int | str replaces Union[int, str] and str | None replaces Optional[str]. Newer code should prefer those.

Optional[str] means "str or None" — it does not mean the argument can be omitted. That is one of the more common misreadings, and defaults are what make an argument optional in the ordinary sense.

mypy is what makes them real

Hints become useful the moment a checker reads them:

pip install mypy
mypy yourfile.py

mypy reports the mismatch that Python allowed at runtime, before you ship it. The failures it catches most usefully are not obvious type errors but None handling — a function declared to return Optional[str] whose result is used without checking for None is exactly the crash that appears in production.

Two practical notes. Adopt it gradually: mypy can check a single module, so you do not have to annotate an entire codebase first. And do not reach for Any, which switches checking off for that value in the same way any does in TypeScript.

Editors use the same information. Hints are what let autocomplete know that a variable is a str and offer string methods, which is a real productivity gain independent of running a checker.

Where to put them and where not to

Annotating everything is not the goal, and over-annotating makes code noisier without adding information.

Worth annotating: function parameters and return values, especially on anything used from more than one place; module-level constants where the type is not obvious; and empty containers, since items = [] tells a checker nothing.

Usually not worth it: local variables with obvious values. count: int = 0 adds nothing over count = 0, because inference already knows.

The highest-value place is a function signature that returns None in some cases. Declaring -> Optional[User] forces every caller to think about the missing case, and that is where the bugs are.

Is it worth it for your project?

Honestly: for a short script, no. The benefit scales with how long the code lives and how many people read it.

Clear wins are libraries other people import, codebases large enough that you cannot hold the shapes in your head, and anything you will return to after a gap — hints are documentation that cannot drift out of date silently, because a checker complains.

The costs are real: more verbose signatures, occasional fights with the checker over something you know is fine, and a genuine learning curve for generics and protocols.

For students, the practical recommendation is to annotate function signatures in project code and skip the rest. It is enough to get editor support and to demonstrate the habit, which is increasingly expected in professional Python.

Frequently Asked Questions

Do type hints make Python code faster? No. They are ignored at runtime and have no effect on performance. The benefit is catching errors before execution and better editor support.
What is the difference between Optional[str] and a default value? Optional[str] means the value may be a string or None. A default value is what makes an argument omittable. They are unrelated, though they often appear together.
Do I need mypy for hints to be useful? Not strictly — editors use them for autocomplete and inline warnings. But without a checker, nothing verifies the hints are accurate, so they can quietly become wrong.
Can Python enforce types at runtime? Not natively. Libraries such as pydantic validate data against type declarations at runtime, which is the standard approach for API input where the data comes from outside your control.
Should I annotate every variable? No. Annotate function signatures and empty containers, where the type is not inferable. Annotating obvious local variables adds noise without information.