Python Type Hints

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: type puts a type on a variable or a parameter.
  • -> type after 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, Alex

So 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.0
user_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 True

You 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 yet
score = 90
print(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.66666666666667

Note

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.com
None

There 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.com

For 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.

Terminal window
pip install mypy

Say 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:

Terminal window
mypy demo.py

mypy 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 back
def get_total(items):
return sum(items)
# ✅ Good: the reader sees the input and the output type
def get_total(items: list[int]) -> int:
return sum(items)
  • Annotating a list but forgetting the inside type. list alone 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 strings
names: list[str] = []
  • Writing Optional[str] and thinking it means “the argument is optional”. It does not mean that. It means the value can be a str or None. 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] over List[int], and str | None over Optional[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 = 0 does not need count: 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. 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. 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. 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. 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.

Named Tuples

Share & Connect