Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ~27 min· topic 1 of 9

Topic 10.1

Type Hints: What They Are and Aren't

In one line

A type hint is a label that says what kind of value a variable, parameter or return value is meant to be. Python stores the label but never enforces it while the program runs; editors and checker programs read it to catch mistakes early.

Think of it like this

The labels on kitchen jars. A jar marked "sugar" tells everyone what belongs inside. The label does not stop you pouring salt in; it only helps you, and anyone who walks into the kitchen later, avoid a mistake. A type hint is a label like that on a variable or a function.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Type
The kind of a value: whole number (int), text (str), list, and so on.
Parameter
A name in the brackets of a def that receives a value when the function is called (Topic 5.1).
Pydantic
A library that checks at run time that data has the shape its type hints describe (Topic 15.4).
Type hint (annotation)
A label written in the code, after a colon or ->, that says what type something should be.
Type checker
A separate program that reads your code without running it and reports places where the labels do not fit.
Enforce
To refuse to continue when a rule is broken. Python does not enforce hints.
Static
Done by reading the code, before it runs. Type checking is static.
Gradual typing
Adding hints a little at a time; unlabelled code is simply not checked.
`Any`
A label meaning "any value at all"; the checker stops checking that value.
`__annotations__`
The dictionary where Python keeps the hints of a function, class or module.

Step by step

01Write the labels

A variable hint is name: type, optionally followed by = value. A function hint labels each parameter and the return value. The arrow -> means "returns". -> None means the function returns nothing useful.

main.pywhole filepython
name: str = "Asha"
age: int = 30

def greet(person: str, times: int = 1) -> str:
    return (f"Hello, {person}! " * times).strip()

def log(message: str) -> None:
    print("LOG:", message)

02Python stores the labels and ignores them

When Python runs a def, it evaluates each hint and saves the results in the function's __annotations__ dictionary. Nothing else happens. The function does not look at that dictionary when it is called.

So the next call is a lie that Python accepts: both arguments are strings, and + happily joins strings. The answer is "23", not 5. Run the first example below and see.

main.pywhole filepython
def add(a: int, b: int) -> int:
    return a + b

print(add("2", "3"))        # 23: no error
print(add.__annotations__)  # the labels, just stored

03Variables: annotation without a value

A bare nickname: str with no = only records the label in the module's __annotations__. It does not create the variable, so reading nickname afterwards raises NameError. This is useful in classes, where dataclasses (Topic 8.8) and Pydantic read exactly these bare declarations to learn the fields (Topic 10.7).

04Who reads the hints

Python itself does not. Everyone else does: editors, checkers and libraries. The diagram shows the flow. The first two readers work before the program runs; the third works while it runs, but only because the library chooses to read the hints.

Who reads the hintsdiagram
Rendering diagram…

05Gradual typing and Any

You are free to label some things and not others. The checker trusts what you labelled and treats unlabelled parameters as Any. Assigning a value of any type to an Any variable is always allowed, and so is using it in any way. That is why Any hides bugs: the checker looks away.

A good habit: label every function's parameters and return type (the public edges of your code) and let the checker work out the types of local variables on its own. That is where hints give the most protection for the least typing.

06Hints are code too

A hint is a real expression that Python evaluates when the function is defined. A typo such as def f(x: Strr) raises NameError at definition time. Hints that refer to a class defined later in the file need quotes or the deferred evaluation of Topic 10.7.

Try it yourself

  1. 1

    Break the promise

    In the first example, call add(1.5, 2.5) and add(None, 1). Predict each result before you run. The float call works (it is not an int, yet + is fine); the None call fails, but with a TypeError from +, not from the hint.

  2. 2

    Peek at the labels

    Add a third parameter c: float = 0.0 to greet in the second example and print greet.__annotations__ again. Notice the dictionary grows, and that defaults are stored separately in __defaults__.

  3. 3

    Create the variable

    Change nickname: str to nickname: str = "Ash" and run it. The NameError disappears because now there is a value, not just a label.

Code & diagrams

Hints are stored, not enforced New tab

The calls with strings and lists break the labels and Python does not mind. Only a type checker would complain.

Sign in to run this example in your browser.

Expected output

add(2, 3) = 5
add('2', '3') = 23
add([1], [2]) = [1, 2]
annotations: {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
Variable annotations and Any New tab

The module keeps its labels in __annotations__. A label without a value creates no variable.

Sign in to run this example in your browser.

Expected output

module annotations: {'name': <class 'str'>, 'age': <class 'int'>, 'nickname': <class 'str'>, 'anything': typing.Any}
name = Asha
NameError: name 'nickname' is not defined
age is now 'thirty'
Hello, Ravi! Hello, Ravi!
params: {'person': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}
default: (1,)
Hints at three speedsdiagram
Rendering diagram…
A real use: hints on a shopping bill New tab

Every function says what it takes and what it gives back. A reader (or a checker) knows prices are decimals and the cart maps item names to prices without opening the function bodies.

Sign in to run this example in your browser.

Expected output

tea        20.00
milk       30.50
bread      45.00
total: 95.5
with 10% off: 85.95
{'prices': list[float], 'discount_percent': <class 'int'>, 'return': <class 'float'>}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Mis-spell a type name in a hint

Define a function with a hint that names a type that does not exist.

main.pywhole filepython
def total(prices: list[Intt]) -> int:
    return sum(prices)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 1, in <module>
def total(prices: list[Intt]) -> int:
^^^^
NameError: name 'Intt' is not defined. Did you mean: 'int'?

Myth vs fact

Myth

Type hints make Python check types, like Java does.

Fact

Python does not check them while running. Only separate tools (mypy, pyright) and libraries that choose to read them (Pydantic) act on hints.

Myth

Hints make the program faster.

Fact

No. CPython ignores them when running, so speed is the same. The gain is fewer bugs and clearer code.

Myth

If a checker passes, the program has no type errors at run time.

Fact

Only for the code that was labelled, and only if the labels are honest. Data from outside (JSON, user input, an LLM reply) can be anything, which is why Pydantic validates at run time.

Myth

Using hints means giving up Python's flexibility.

Fact

Gradual typing keeps both: label the stable parts and leave quick scripts bare. Duck typing lives on in Protocols (Topic 10.6).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Annotations are stored on the object: functions in __annotations__, classes in Class.__annotations__, modules in __annotations__. Reading them is the basis of every validation library. Use typing.get_type_hints() rather than reading the raw dictionary, because it resolves string annotations (Topic 10.7).

  • ▸

    A hint costs a little time at import: each annotation expression is evaluated when the function is defined. On huge codebases this was one reason for deferred evaluation, which became the default behaviour in Python 3.14.

  • ▸

    In the browser runner on this site and on your computer the behaviour is identical: hints are inert unless something reads them. Do not rely on them for security checks such as "this is always an int"; validate untrusted input explicitly.

  • ▸

    A subtle default: mypy skips the body of a function that has no annotations at all (it is treated as untyped), so a bug inside it goes unreported until you add a hint or turn on --check-untyped-defs (Topic 10.3 covers strictness). A function with even one annotation is checked in full. Any is also different from object: a value of type object accepts anything in, but the checker lets you do almost nothing with it until you narrow it (isinstance), while Any lets you do anything. When you truly do not know the type, object is the honest choice.

  • ▸

    Hints are metadata, so tools can read them without running your code. A module's __annotations__, a class's field list for dataclass, and FastAPI's request parsing are all the same mechanism: typing.get_type_hints() or, from Python 3.14, annotationlib.get_annotations() (Topic 10.7). The interpreter itself never acts on hints; in the standard library, dataclass and NamedTuple read the class-level labels to find the fields, and even they do not check the types.

Remember this

  1. 1

    You write a hint after a colon: age: int = 30 labels the variable age as an int. For functions, each parameter (a name in the brackets that receives a value, Topic 5.1) gets name: type. The result gets -> type after the brackets: def add(a: int, b: int) -> int:. The label can be any name or phrase that stands for a type, such as int, str, list[int] or a class you wrote.

  2. 2

    The most important fact of this whole phase: Python does not enforce hints. Python reads the labels, stores them (functions keep them in __annotations__) and then ignores them. add("2", "3") runs happily and returns "23", because + joins two strings. A hint is a promise to readers, not a lock on the door.

  3. 3

    So who reads the labels? Three audiences. People reading the code know what to pass in. Editors such as VS Code use the hints to suggest what to type next and to draw red underlines. Type checkers (programs such as mypy and pyright, Topic 10.3) read your whole project without running it and report every place where a value of the wrong kind is passed.

  4. 4

    This approach is called gradual typing: you can add hints to some functions and leave others bare, and the checker only complains where the labels contradict each other. You can start with the functions that matter most and add more later, which is why large existing codebases can adopt hints one file at a time.

  5. 5

    The special label Any means "anything goes". A checker will accept any value where Any is expected and will not check what you do with it. It is the escape hatch, and a codebase full of Any is a codebase without protection. Prefer a real type; use Any only where the shape truly cannot be described.

  6. 6

    Why do teams bother? Hints turn a whole class of crashes ("None has no attribute", "can only concatenate str to int") into errors you see in the editor before the program ever runs. They are also documentation that cannot go out of date quietly, because the checker fails when it does.

  7. 7

    In AI work hints have a second job. Pydantic (a library that checks data, Topic 15.4) and LLM tool-calling libraries read the hints on your classes to build validators and JSON schemas, which are written descriptions of the shape of data (Topic 10.7).

Explain it without notes

01

Does Python enforce type hints? What reads them, then?

02

What is gradual typing and why does it matter for existing codebases?

03

What does Any do for a type checker, and why is it risky?

04

What does name: str with no value do?

05

What is the difference between Any and object as a hint?

Practice

01

Write area(width: float, height: float) -> float and print area.__annotations__.

02

Write shout(text: str) -> str that returns the text in capitals, then call it with the number 5. What happens, and does the hint stop it?

03

Declare count: int without a value and show what happens when you print count.

Trade-offs

  • ↔

    More typing for less debugging. Hints cost a few seconds per function and save hours finding None and wrong-type bugs later. The gain grows with the size of the codebase and the number of people.

  • ↔

    Checked at the edges, trusted inside. Hints protect code you control. They do not validate data arriving from outside, so you still need run-time validation there (Topic 10.7).

  • ↔

    Precision versus noise. Very detailed hints can be harder to read than the code. Aim for clear, not clever; start with function signatures.

Done when you can

  • Done when you can write hints for variables, parameters and return values.

  • Done when you can show with a short program that Python does not enforce hints.

  • Done when you can name the three kinds of reader of a hint (people, checkers, libraries).

  • Done when you can explain gradual typing and the risk of Any.

  • Done when you can find a function's hints in __annotations__.