Writing Pythonic CodeIntermediate8 min59 / 63

Type Hints

Learn how to add optional type annotations to your Python code to make it clearer, easier to maintain, and less bug-prone.

Python is a dynamically typed language, which means you can write a function and pass any value you like — Python won't stop you. That freedom is great when you're exploring, but it can cause confusion later: What kind of value does this function expect? What does it return?

Type hints are Python's answer. They let you label your variables and functions with the kinds of values they work with. Think of them as sticky notes attached to your code that say "this should be a string" or "this returns a number."

See it in action

Visual walkthrough1 / 5
1

Labels for Your Code

Python lets you write functions without saying what types they expect — which is flexible, but can get confusing fast. Type hints are optional labels that say exactly what kind of values a function needs and returns.

Python never enforces type hints at runtime — they're for you, your editor, and tools like mypy.
Note

Type hints are optional

Python itself never enforces type hints at runtime. Your program runs exactly the same whether you add them or not. Their power comes from tools like editors (VS Code, PyCharm) and the static checker mypy, which read the hints and warn you about mistakes before you even run your code.

#Annotating Function Parameters and Return Values

The most common place to add type hints is on function definitions. You write the expected type after a colon following each parameter name, and you write the return type after -> before the colon that ends the def line.

name: str means the function expects a string. -> str means it returns a string.
def greet(name: str) -> str:
    return "Hello, " + name

result = greet("Alice")
print(result)

Let's add another example with numbers:

  • age: int — expects a whole number
  • -> float — returns a decimal number
Both parameters are annotated as int, and the return type is also int.
def years_until_retirement(age: int, retirement_age: int) -> int:
    return retirement_age - age

print(years_until_retirement(30, 65))

#Annotating Variables

You can also annotate plain variables. The syntax is variable_name: type = value. This is useful when you declare a variable before you know its value, or when the type might not be obvious at a glance.

Variable annotations make the intended type explicit right at the declaration.
count: int = 0
name: str = "Bob"
price: float = 9.99
is_active: bool = True

print(count, name, price, is_active)

#Common Built-in Types

The most common types you will annotate with are Python's built-in ones:

  • str — text
  • int — whole numbers
  • float — decimal numbers
  • boolTrue or False
  • list — a list
  • dict — a dictionary
  • tuple — a tuple
  • None — the absence of a value (use as a return type when a function returns nothing)
-> None means the function does not return a meaningful value.
def log_message(message: str) -> None:
    print("[LOG]", message)

log_message("Server started")

#Typing Collections: list, dict, and More

Knowing that a variable is a list is useful, but knowing it is a list of integers is even better. Since Python 3.9, you can write the item type right inside square brackets.

list[int] means a list where every item is an integer.
def total(scores: list[int]) -> int:
    return sum(scores)

print(total([10, 20, 30]))
dict[str, int] means keys are strings and values are integers.
def word_lengths(words: list[str]) -> dict[str, int]:
    return {word: len(word) for word in words}

print(word_lengths(["cat", "elephant", "ox"]))
Think of it like

Think of it like a labelled box

Imagine a moving box labelled "Books — Paperback". You could technically put anything inside, but the label tells everyone what belongs there and makes sorting much easier. Type hints are those labels on your code's boxes.

#Optional Values: X | None

Sometimes a value might be present or it might be missing (None). Python 3.10 introduced a clean way to express this: X | None. This is called a union type — it means the value is either of type X or it is None.

In older Python (3.9 and below) you would write Optional[X] from the typing module, but X | None is now preferred.

str | None signals that callers must handle the possibility of None.
def find_user(user_id: int) -> str | None:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)  # returns None if not found

print(find_user(1))
print(find_user(99))
Optional[str] is equivalent to str | None. You will see both in real code.
# Older style using typing.Optional (still valid)
from typing import Optional

def get_nickname(name: str) -> Optional[str]:
    nicknames = {"Robert": "Bob"}
    return nicknames.get(name)

print(get_nickname("Robert"))
print(get_nickname("Alice"))

#Why Bother? The Real Benefits

You might be thinking: "If Python ignores them, why add them at all?" Great question. Here are the concrete benefits:

  • Better autocomplete — your editor knows what methods are available on a value.
  • Catch bugs early — tools like mypy can spot type mismatches before you run the code.
  • Self-documenting code — reading a function signature tells you exactly what it needs and gives back.
  • Safer refactoring — when you change a function's input type, mypy flags every place that breaks.
Common mistake

Python does NOT enforce type hints at runtime

This trips up many beginners. Even with type hints, Python will not raise an error if you pass the wrong type at runtime.

```python def double(n: int) -> int: return n * 2

print(double("ha")) # prints 'haha' — no error! ```

Type hints are only checked by external tools like mypy. If you need runtime enforcement, look at libraries like pydantic.

Tip

Run mypy to check your hints

Install mypy with pip install mypy, then run mypy your_file.py from the terminal. It will report any type mismatches it finds — all without running your program.

#Putting It All Together

A realistic function with annotated parameters, a list type, and a complex return type.
def summarize_scores(name: str, scores: list[int]) -> dict[str, int | float]:
    total = sum(scores)
    average = total / len(scores) if scores else 0.0
    return {"total": total, "average": average}

result = summarize_scores("Alice", [85, 92, 78])
print(result)
Quick check

What happens when you call a Python function with the wrong type — for example, passing a string where an int is annotated?

Key takeaways

  • Type hints let you annotate functions and variables with expected types using `name: type` and `-> return_type` syntax.
  • Python never enforces type hints at runtime — they exist purely for editors, tools like mypy, and human readers.
  • Use `list[int]`, `dict[str, int]`, and similar forms to describe collections with typed contents.
  • Use `X | None` (or `Optional[X]`) when a value might be absent.
  • Type hints make code self-documenting, improve autocomplete, and help catch bugs before you run your program.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

What does this code print?

predict-output
def add(a: int, b: int) -> int:
    return a + b

print(add(3, 4))
Predict the output#2

Python does NOT enforce type hints at runtime. What does this code actually print?

predict-output
def repeat(text: str, times: int) -> str:
    return text * times

print(repeat("ha", 3))
Fix the bug#3

This function is meant to return the length of a list of scores, but the return type hint is wrong. What should be fixed?

fix-bug
def count_scores(scores: list[int]) -> str:
    return len(scores)
Fill in the blank#4

Complete the function signature so it accepts a string name and returns a string, then fill in the return type for a function that prints a log and returns nothing.

def greet(name: ) -> :
    return "Hi, " + name

def log(msg: str) -> :
    print(msg)
Your turn
Practice exercise

Write a function called format_greeting that takes a name (str) and an optional title (str or None, defaulting to None). If a title is provided, return "Hello, Dr. Alice" (or whatever title+name). If no title, return "Hello, Alice". Add full type hints to the function signature.

Try it live — edit the code and hit Run to execute real Python:

solution.py · editable