Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Fundamentals  ›  Lesson

Type Annotations

Type Annotations 12 minLabelling the types your functions expect and return
You're building a piece ofBill Splitter & Tip Calculator
This piece — receipt_line(): Formats one line of the printed receipt.
Scenario Every line on the printed receipt reads 'Dish: $price'. This formats one line — and we annotate it so anyone reading the code knows exactly what goes in and what comes out.
Your task
Build receipt_line(name, price) — WITH type annotations — that returns one receipt line as 'name: $price' (price to 2 decimals). Annotate it: name is a str, price is a float, it returns a str. Example: receipt_line("Soup", 10) → "Soup: $10.00".

Type Annotations

Type annotations (or type hints) label the types a function expects and returns. They don't change how your code runs — Python ignores them at runtime — but they make code far easier to read, and they power editor autocomplete and tools like mypy that catch type bugs before you run. Modern Python code is full of them. This lesson builds one annotated receipt-line formatter.

Every box is live — edit and ▶ Run.


Part 1 — Annotating a variable

Add : type after a name to say what it should hold:

python

The : str, : int, … are the annotations. The code runs exactly as it would without them — they're notes about intent.


Part 2 — Annotating a function

This is where annotations earn their keep. Put : type on each parameter, and -> type before the colon for the return type:

python

Read the signature as: "greet takes a str called name and returns a str." You know how to call it correctly without reading the body.


Part 3 — Annotating collections

You can be specific about what's inside a list, dict, or tuple:

python

list[float] = "a list of floats". dict[str, float] = "a dict with string keys and float values". Clear at a glance.


Part 4 — "Maybe nothing": | None

Sometimes a value might be missing. str | None means "a str or None":

python

Part 5 — They're hints, not handcuffs

Crucially, annotations are not enforced at runtime. Python won't stop you from passing the "wrong" type — the annotation is just documentation:

What does this print?
def double(n: int) -> int:
    return n * 2

print(double("ab"))
python

That's why the task below is checked by its behavior: write the annotations correctly for readability, and make the function return the right string.


Part 6 — Idioms & real-world patterns

The | union you just saw is the modern style. Here's the rest of the toolkit you meet in real, typed codebases.

Optional, Union, and tuples — the typing names

X | None is the same as Optional[X]; A | B is the same as Union[A, B]. The | form is newer (3.10+); the typing names are everywhere in existing code:

python

Annotate a fixed-shape tuple with the type of each slot:

python

TypeAlias, Callable, Any

Give a complex type a name so signatures stay readable, and type the functions you pass around with Callable[[args], return]:

python

Coming from Java/TS: these map cleanly — X | None ≈ TS X | null, Optional[X] ≈ TS optional, TypeAlias ≈ TS type Menu = …, Callable[[float], float] ≈ TS (x: number) => number. The key difference: Python does not enforce them at runtime — that's mypy's job.

❓ Cross-question — "So Optional[str] is an optional field, like TS str??" The #1 TS-dev trap. Optional[X] means X | None (the value can be None) — it does not make a key or argument optional. "Optional key" is a different thing: NotRequired in a TypedDict, or a parameter default (def f(x=None)). A field can be required and Optional (must be present, but may be None).

⚡ Advanced — @dataclass: a typed record with no boilerplate

A dataclass turns a few typed fields into a full class — __init__, repr, and == are generated for you. This is how you model domain objects:

python

Coming from Java: it's a record (or Lombok @Data) — fields plus a generated constructor/equals/toString. Add frozen=True for an immutable value.

⚡ Advanced — Literal, TypedDict, Protocol

For expert-level, self-documenting APIs:

python

Coming from Java/C#: Protocol is a structural interface — a class satisfies it just by having the right methods (duck typing), no implements. Literal is a lightweight enum; TypedDict types a JSON-ish dict.


Part 7 — Deep typing: dicts, tuples & sets (for TS devs)

Coming from TypeScript, you'll want to type shapes, not just dict/tuple/set. Python's tools map almost 1:1 to TS — here's the deep end.

Dicts, two ways: TypedDict (fixed shape) vs dict[K, V] (uniform)

A TypedDict types a dict whose keys are known — like a TS interface. It nests, and NotRequired marks optional keys:

python

❓ Cross-question — "How do I make ALL keys optional, like TS Partial<T>?" Set total=False on the class: class Draft(UserProfile, total=False) makes every key optional. The inverse — one must-have key inside a total=False dict — is Required[...]. Per-field, NotRequired/Required ≈ TS ?:.

When keys are dynamic (not known ahead of time), type the value with a union — this is TS's Record<K, V>:

python

❓ Cross-question — "TypedDict vs dataclass — which one?" Use a TypedDict when the data really is a plain dict (e.g. JSON from an API) and you want dict access. Use a @dataclass when you want a real object with methods, defaults, and obj.field access. TS analogy: interface for a shape you pass around vs class for behaviour.

Tuples: fixed shape vs variable length

tuple[int, str, bool] is a fixed 3-slot tuple (each slot typed) — TS's [number, string, boolean]. tuple[int, ...] (with the literal ...) means "a tuple of any length, all int" — TS's number[]:

python

❓ Cross-question — "Is tuple[int, str] a readonly tuple like in TS?" Yes — tuples are immutable, so tuple[int, str] ≈ TS readonly [number, string]. For named slots (the TS "object-ish tuple"), use a NamedTuple.

Sets: set[T] and frozenset[T]

python

Remember: a set's element type (and a dict's key type) must be hashable/immutable — there's no set[list[int]]. (See Lesson 11.)

Name gnarly types with TypeAlias

python

❓ Cross-question — "Do any of these run-check the shape?" No — like TypeScript, Python hints are erased at runtime; passing the wrong shape won't raise. A checker (mypy or pyright — pyright is the engine behind your TS editor tooling) flags it before you run. Hints document and tool-check; they don't enforce.


🎯 Your turn

Write receipt_line(name, price) with type annotations — it returns one line of the receipt as "name: $price" with the price to two decimals:

  • receipt_line("Soup", 10)"Soup: $10.00"
  • receipt_line("Steak", 20.5)"Steak: $20.50"

Hint — annotate the signature def receipt_line(name: str, price: float) -> str: then return f"{name}: ${price:.2f}".

Then press ▶ Run, tap the Live App try chips, and hit ✓ Check. Green = this piece of the app is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upCount words (typed)

Write count_words(text: str) -> int returning how many whitespace-separated words text has.

annotations
DrillActive users

A user is {"name": str, "active": bool}. Write active_names(users) returning the names of active users only. Type it with a TypedDict.

TypedDictcomprehension
BuildCart total (typed)

A cart item is {"name": str, "price": float, "qty": int}. Write total_price(cart) returning the grand total (price × qty summed), rounded to 2 decimals. Type it.

TypedDictgeneratorround
BossOrder summary (typed)

An order is {"table": int, "total": float}. Write order_summary(orders) returning {"tables": <count>, "revenue": <sum of totals>, "biggest": <the order with the max total>}. Empty → counts 0 and "biggest": None.

TypedDictmax-keyguard
CapstoneGroup orders by table (typed)

An order is {"table": str, "dish": str}. Write group_orders(orders) returning a dict mapping each table id to its list of dishes, order preserved. Type it.

TypedDictsetdefault
receipt_line.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
receipt_line(name, price) → strOne formatted line of the printed receipt.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.
Type Annotations — Pebells