Python Type Hints: Syntax and Examples for Beginners
Table of Contents
I started coding in middle school, and back then I didn't even have syntax highlighting enable, but I'm so spoiled now that I can't be botheredt to read a function just to learn whether it takes an integer or a string.
Python type hints declare those sorts of expectations directly in the code (at least, as much as Python can)! They make function signatures easier to understand, improve editor autocomplete, and let static type checkers catch mismatches before they turn into runtime bugs.
All the content from our Boot.dev courses is available for free here on the blog. This guide comes from the "Type Hints" chapter of Learn Python for Beginners, where the interactive editor checks your types while you write code.
What Are Type Hints in Python?
When a program is small, you can usually remember the types of your variables. But as programs grow, it's easy to forget:
- Is
levelanintor astr? - Does
get_item()always return an item name, or sometimesNoneif it can't find one? - Is
inventorya list of strings or a dictionary of item counts?
Type hints let us write those expectations directly in our code:
def get_damage(weapon: dict[str, int], level: int) -> int:
return weapon["damage"] + (level * 2)
The weapon: dict[str, int], level: int, and -> int parts are type hints. They tell humans, code editors, and static type checkers what kinds of values the function expects and returns.
Type hints don't make Python stop being Python. It's still a dynamically typed language, and it won't automatically reject the wrong value because a type hint says so.
In everyday Python, type hint and type annotation are often used interchangeably. More precisely, an annotation is the label attached to a variable, parameter, or return value. The type hint is the expectation that label communicates.
Type Hints for Variables
To add a type hint to a variable, put a colon after the variable name, then the type. This comes before the equals sign and the value:
character_name: str = "Sir Galahad"
character_level: int = 7
character_health: float = 72.5
has_magic: bool = True
The values work the exact same way they did before. The annotation gives tools more information, but it doesn't convert or validate the value.
When the assigned value makes the type obvious, you usually don't need to repeat it:
character_health = 72.5
Your tooling can infer that character_health is a float. so avoid explicit variable annotations unless they add information that couldn't be inferred... inference is awesome.
Your own classes work as types too:
class Player:
pass
active_player: Player = Player()
Function Parameter and Return Type Hints
Function boundaries are often where type hints help me most. Put each parameter's type after its name, then put the return type after the parameter list with ->:
def add_gold(current_gold: int, found_gold: int) -> int:
return current_gold + found_gold
The -> int means the function is expected to return an integer. When you know what types go into and come out of a function, you can probably use it without needing to read every line of the function body, which is particularly useful when you're using a library's functions, or working in a massive codebase.
Use -> None when a function performs work but doesn't return a useful value:
def print_status(message: str) -> None:
print(message)
A default value and a type hint answer different questions. The hint says which values the parameter accepts, and the default parameter says what Python should use when the caller omits it:
def greet_player(name: str = "traveler") -> str:
return f"Welcome, {name}!"
Type Hints for Lists, Sets, Dictionaries, and Tuples
Container types hold other values, so their hints can describe both the container and its contents. Modern Python puts the contained types in square brackets:
| Type Hint | Meaning |
|---|---|
list[str] |
A list containing strings |
set[str] |
A set containing strings |
dict[str, int] |
A dictionary with string keys and integer values |
tuple[str, int] |
A two-value tuple containing a string and integer |
A Python list and set each take one contained type:
inventory: list[str] = ["Iron Sword", "Healing Potion"]
unique_items: set[str] = {"Iron Sword", "Healing Potion"}
A dictionary maps keys to values, so its hint needs two types:
item_counts: dict[str, int] = {
"Wooden Arrow": 30,
"Small Amethyst": 2,
}
The first type is for the keys and the second is for the values.
Tuples are a small fixed group of values where each position has its own meaning. Because they're fixed, it's common for those values to have different types:
drop: tuple[str, int] = ("Garnet Mark", 2)
tuple[str, int] means the first value is a string and the second is an integer. For a variable-length tuple where every value has the same type, use an ellipsis: tuple[str, ...].
Be Specific About Container Types
This does work:
items: list = ["Black Firebomb", "Titanite Chunk"]
But list doesn't tell your tools what kind of values it contains! Assuming you know what's inside, just be specific:
items: list[str] = ["Black Firebomb", "Titanite Chunk"]
Bare container hints aren't wrong. Sometimes you genuinely don't know what types a container will hold, or the precise hint would be too complicated to help. The Any type makes that choice explicit, but it also tells a checker to allow any operation on the value:
from typing import Any
mixed_items: list[Any] = ["Titanite Chunk", 3, None]
Every Any gives up some specificity when type checking. I like to treat it as an escape hatch, and only use it when I genuinely don't know what type a value will be.
Container hints can also nest when one container holds another container:
character_spells: dict[str, list[str]] = {
"Gandalf": ["Fireball", "Light"],
"Frodo": ["Hide"],
}
Read dict[str, list[str]] from the outside in:
- It's a dictionary
- Each key is a string
- Each value is a list of strings
Nested types can get super confusing, but honestly, they're less confusing than the data would be without the typing.
Optional Values and Multiple Types
Sometimes a value may not exist. Use the union operator to list the types it can have:
damage_bonus: int | None = None
int | None means damage_bonus can be an integer or None. A function can use the same syntax when it may not find a result:
def get_prepared_spell(has_spell: bool) -> str | None:
if has_spell:
return "Fireball"
return None
The | operator isn't limited to None. int | str means either an integer or a string. The sum types guide covers unions in more depth when the alternatives represent distinct cases in a program.
An optional value and an optional argument aren't the same thing. str | None allows None; adding = None lets the caller omit the argument:
def find_player(name: str | None = None) -> str:
if name is None:
return "No player selected"
return name
Modern and Legacy Type Hint Syntax
Python's type-hint syntax has gotten much nicer. Built-in container generics like list[str] require Python 3.9 or newer, and int | None requires Python 3.10 or newer.
| Modern Syntax | Older Equivalent | Modern Form Added |
|---|---|---|
list[str] |
List[str] |
Python 3.9 |
dict[str, int] |
Dict[str, int] |
Python 3.9 |
str | None |
Optional[str] |
Python 3.10 |
int | str |
Union[int, str] |
Python 3.10 |
The older names come from the typing module:
from typing import Dict, List, Optional, Union
names: List[str] = ["Gandalf", "Frodo"]
scores: Dict[str, int] = {"Gandalf": 100}
selected_name: Optional[str] = None
player_id: Union[int, str] = "wizard-1"
Use modern syntax when your supported Python version allows it. You'll still encounter the older forms in libraries and codebases that support older Python releases.
Does Python Enforce Type Hints?
No. The Python runtime doesn't enforce function or variable annotations. Here's a screenshot showing the static analysis error:
But the code does run, it just produces a result that doesn't match the type hint:
def double(level: int) -> int:
return level * 2
x = double("7")
print(x)
# "77" (string repetition, not integer multiplication)
A static type checker catches the incompatible string argument without running the program. ty, mypy, and Pyright are common options; many editors run one in the background and show errors as you type. So, most serious codebases won't allow code to be merged to production unless the code passes a type checker!
You can run ty from a project directory with uvx:
uvx ty check
Fix the Code or Fix the Hint
The whole point of type hints is that they should match what the code actually does. When type hints and code behavior disagree, one of them is wrong.
This function promises a list of (quest_name, quest_xp) tuples, but it appends only the XP integer:
def summarize_quest_rewards(
completed_quests: list[str],
quest_rewards: dict[str, int],
) -> list[tuple[str, int]]:
summary = []
for quest_name in completed_quests:
quest_xp = quest_rewards[quest_name]
summary.append(quest_xp)
return summary
A checker can flag the mismatched return type before the bad shape reaches another function. If the signature is correct, you gotta fix the implementation:
summary.append((quest_name, quest_xp))
If the implementation is correct, fix the hint instead. An incorrect type hint is confusing at best.
When Should You Use Type Hints?
My rule is simple: start with function parameters and return types. Unlike an obvious local assignment, a function boundary can't always be inferred without reading the implementation or running the code.
Type hints should be used when:
- A function is called from several places
- A value can be missing or have more than one type
- A container's nested shape isn't obvious
- Other developers or tools need to understand the interface
Don't annotate every local variable just because you feel a sense or personal obligation. Add hints where they communicate information, keep them accurate, and let tooling infer what it can. Python supports gradual typing, so you can start at the boundaries and add detail as the program grows.
Frequently Asked Questions
What are type hints in Python?
Type hints are annotations that describe the types a variable, function parameter, or return value is expected to have. They help people, editors, and static type checkers understand the code.
Does Python enforce type hints at runtime?
No. Python still runs dynamically typed code even when its type hints are wrong. A static type checker like ty, mypy, or Pyright reports mismatches before you run the program.
What is the difference between a type hint and a type annotation?
An annotation is the syntax attached to a variable, parameter, or return value. A type hint is the expected type that annotation communicates. Python developers often use the terms interchangeably.
How do you type hint a list or dictionary in Python?
On Python 3.9 and newer, write the contained types in square brackets. For example, list[str] is a list of strings, and dict[str, int] is a dictionary with string keys and integer values.
Are type hints required in Python?
No. Type hints are optional. They are most useful at function boundaries and in code that other people, editors, or static type checkers need to understand.
