Python Type Hints
Table of Contents + −
In the last lesson you learned about Dataclasses. There you wrote things like name: str and age: int right next to each field. That little : str part has a name. It is called a type hint. In this lesson we will look at what those hints really do. We will see where you can use them, and why they make your code easier to read and safer to change.
🤔 Why type hints?
Picture this. Alex opens a function someone else wrote six months ago:
def send_reminder(user, count): ...What is user here? A name string? A whole user object? An id number? And count, is that a number or could it be a list? You cannot tell. So you go and read the whole function. Then you read everything that calls it, just to work out what to pass in. That is the pain.
Type hints fix this in one line. They let you write down what type each value is meant to be. Now the answer is right there in the signature.
def send_reminder(user: str, count: int) -> None: ...Now anyone reading it knows user is a string and count is a whole number. They also know the function returns nothing. No detective work needed.
Note
Type hints are notes for humans and tools. They do NOT change how Python runs your code. Python ignores them when it runs. We will prove this in a moment.
🧩 What a type hint actually is
A type hint is a small label you attach to a variable, a parameter, or a return value. It says “this is supposed to be a string” or “this should be a number”.
Think of it like the label on a food container in the fridge. The label says “soup”. The container does not stop you from putting salad in it. But everyone who opens the fridge knows what is meant to be inside. And if they find salad in the “soup” box, they know something went wrong. Type hints work the same way. They describe what you meant. They do not force it when the code runs.
Two pieces of syntax do most of the work:
name: typeputs a type on a variable or a parameter.-> typeafter the parentheses puts a type on what the function returns.
✍️ Function annotations
The most common place you will see type hints is on function parameters and return values. Here is a function that adds two numbers. Every part of it is annotated.
def add(a: int, b: int) -> int: return a + b
result = add(3, 5)print(result)print(type(result))Read it left to right. a: int means the first parameter should be a whole number. b: int says the same. And -> int means the function hands back a whole number. The body is normal Python.
Output
8<class 'int'>Now the important part. Python does not check those hints. Watch what happens when we pass strings instead.
def add(a: int, b: int) -> int: return a + b
print(add("Hello, ", "Alex"))We said int, but Python happily runs it and joins the strings. That works because + works on strings too.
Output
Hello, AlexSo the hint said “integers expected”. Python did not care. And the code still ran. The hint is a note, not a rule. That is the single most important thing to remember about type hints.
🔢 Variable annotations
You can also put a type on a plain variable, not just on function parameters. This is handy when the value is not obvious from the line itself.
total: float = 0.0user_name: str = "Riya"is_active: bool = True
print(total, user_name, is_active)The pattern is variable: type = value. The : type part is the hint. The = value part is the normal assignment.
Output
0.0 Riya TrueYou can even annotate a variable without giving it a value yet. This just records the intended type for readers and tools.
score: int # declared, not assigned yetscore = 90print(score)Output
90📦 Hints for collections
Saying something is a list is fine. But often you want to say what is inside the list. Modern Python lets you write that directly with square brackets.
scores: list[int] = [90, 85, 100]names: list[str] = ["Alex", "Riya", "Arjun"]prices: dict[str, float] = {"coffee": 2.5, "tea": 1.8}
print(scores)print(names)print(prices)Read these out loud. list[int] is “a list of integers”. list[str] is “a list of strings”. And dict[str, float] is “a dictionary whose keys are strings and whose values are floats”.
Output
[90, 85, 100]['Alex', 'Riya', 'Arjun']{'coffee': 2.5, 'tea': 1.8}Here is a function that uses one of these. It takes a list of integers and returns their average as a float.
def average(numbers: list[int]) -> float: return sum(numbers) / len(numbers)
print(average([90, 85, 100]))Output
91.66666666666667Note
The list[int] and dict[str, float] style works in Python 3.9 and newer. On older versions you had to import List and Dict from the typing module and write List[int]. For new code, the lowercase built-in style is the one to use.
❓ When a value might be missing
Sometimes a value is either there or it is None. A common case is a lookup that might not find anything. You want to say “this returns a string, or it might return nothing”.
The modern way to write that is str | None. The | means “either of these types”.
def find_email(user_id: int) -> str | None: directory = {1: "alex@example.com", 2: "riya@example.com"} return directory.get(user_id) # returns None if the id is missing
print(find_email(1))print(find_email(99))The return hint str | None tells the reader what to expect. You get a string when the user exists, and None when they do not. So the reader knows to check for None before using the result.
Output
alex@example.comNoneThere is an older name for the exact same idea. It is Optional[str], which you import from typing. Optional[str] and str | None mean precisely the same thing.
from typing import Optional
def find_email(user_id: int) -> Optional[str]: directory = {1: "alex@example.com"} return directory.get(user_id)
print(find_email(1))Output
alex@example.comFor new code on Python 3.10 and newer, prefer str | None. It needs no import and it reads naturally.
🛠️ Where the checking actually happens
If Python ignores the hints, who reads them? Tools do. Your code editor reads them to give you better autocomplete and to underline a mistake while you type. And a separate program called mypy reads them to check your whole project for type mismatches, before you ever run it.
mypy is a third-party tool. You install it with pip.
pip install mypySay you have this file, demo.py. The function wants an int, but we call it with a string.
def double(n: int) -> int: return n * 2
print(double("oops"))Run the checker on it from your terminal:
mypy demo.pymypy reads the hints. It sees that "oops" is a str where an int was promised. So it reports the problem, all without running the program.
Output
demo.py:4: error: Argument 1 to "double" has incompatible type "str"; expected "int" [arg-type]Found 1 error in 1 file (checked 1 source file)That is the real payoff. The mistake is caught early, by a tool. It does not turn into a strange bug later when real users hit it.
📋 Common type hints at a glance
Here are the hints you will reach for most often.
| You want to say | Write this |
|---|---|
| A whole number | int |
| A decimal number | float |
| Text | str |
| True or False | bool |
| A list of integers | list[int] |
| A dictionary of name to score | dict[str, int] |
| A string or nothing | str | None |
| Function returns nothing | -> None |
⚠️ Common Mistakes
A few things trip people up when they start adding hints.
-
Thinking the hint forces the type. It does not. Python still runs code that breaks the hint. Only a checker like mypy or your editor flags it.
-
Forgetting the return hint when a function gives something back. If it returns a value, say so with
-> type. If it truly returns nothing, write-> None.
# ❌ Avoid: unclear what comes backdef get_total(items): return sum(items)
# ✅ Good: the reader sees the input and the output typedef get_total(items: list[int]) -> int: return sum(items)- Annotating a list but forgetting the inside type.
listalone is vague.list[str]tells the reader and the tools what each item is.
# ❌ Avoid: a list of what?names: list = []
# ✅ Good: a list of stringsnames: list[str] = []- Writing
Optional[str]and thinking it means “the argument is optional”. It does not mean that. It means the value can be astrorNone. Whether an argument has a default is a separate thing.
✅ Best Practices
A few simple habits make hints worth the effort.
-
Start with the public functions. Hinting the parameters and return of the functions other people call gives you the most value for the least work.
-
Prefer the modern style. Use
list[int]overList[int], andstr | NoneoverOptional[str], on Python 3.10 and newer. -
Run mypy as part of your checks. Hints only catch bugs if something actually reads them. So let a tool read them.
-
Do not over-annotate obvious local variables.
count = 0does not needcount: int = 0. Add hints where they remove real confusion, not everywhere.
🧩 What You’ve Learned
✅ A type hint like name: str or -> int writes down the type a value is meant to be.
✅ Hints do NOT change how Python runs. Python ignores them, so wrong types still run.
✅ You can annotate parameters, return values, and plain variables.
✅ list[int] and dict[str, float] describe what is inside a collection.
✅ str | None (or the older Optional[str]) means a value can be a string or nothing.
✅ Tools like your editor and mypy read the hints and catch type mistakes early, before the code runs.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does Python do with type hints when it runs your code?
Why: Type hints are notes for humans and tools. Python does not enforce them at runtime, so mismatched types still run.
- 2
What does the hint in `def average(numbers: list[int]) -> float:` say?
Why: `numbers: list[int]` means a list of integers, and `-> float` means the function returns a float.
- 3
What does the return hint `str | None` mean?
Why: `str | None` means the value is either a string or None. It is the same idea as Optional[str].
- 4
Which tool reads your type hints to catch mismatches before the code runs?
Why: mypy is a third-party type checker that reads your hints and reports type mismatches without running the program.
🚀 What’s Next?
Now that you can describe types clearly, the next step is a neat little tool. It gives plain tuples named fields and built-in type hints, so your data reads like a real record.