Learn Python Series (#36) - Type Hints and Modern Python

in #stemyesterday

Learn Python Series (#36) - Type Hints and Modern Python

python-logo.png

Repository

What will I learn?

  • You will learn the conceptual difference between dynamic and static typing;
  • why type hints exist in Python even though the language doesn't enforce them;
  • how to think about type annotations as documentation that tools can verify;
  • the mental model behind Optional and Union types;
  • how dataclasses combine type hints with clean data structures.

Requirements

  • A working modern computer running macOS, Windows or Ubuntu;
  • An installed Python 3(.11+) distribution;
  • The ambition to learn Python programming.

Difficulty

  • Intermediate

Curriculum (of the Learn Python Series):

GitHub Account

https://github.com/realScipio

Learn Python Series (#36) - Type Hints and Modern Python

Python has always been dynamically typed. You can write x = 5 and then later write x = "hello" without Python complaining. The variable doesn't have a fixed type - it's just a name pointing to whatever value you assign.

This flexibility is powerful, but it comes with a cost. When you're reading code, you can't tell what type a variable holds just by looking at it. When you're writing code, your editor can't warn you if you're calling a method that doesn't exist. And when you're debugging, type-related bugs only show up when that specific line of code runs.

Nota bene: This episode is about type hints (also called type annotations), a feature introduced in Python 3.5 that lets you optionally declare types in your code. The key word is "optionally" - Python still doesn't enforce types at runtime. But tools can check them for you before you run the code.

The mental model: types as documentation that tools verify

Think of type hints like comments, but smarter. When you write a comment explaining what a function expects, nobody checks if your comment is accurate. It might be wrong, or outdated. Type hints are different: tools like mypy can verify that your hints match how you actually use the code.

Here's the same function without and with type hints:

def greet(name):
    return f"Hello, {name}!"

With a type hint, you declare that name should be a string, and that this function returns a string:

def greet(name: str) -> str:
    return f"Hello, {name}!"

The syntax is: parameter colon type, arrow return-type. That's it. Python won't stop you from calling greet(42), but your editor will show a warning, and mypy will catch it before runtime.

Nota bene: Type hints don't make Python slower. They're ignored at runtime. They exist purely for tooling - your IDE, your linter, your type checker.

Why bother with type hints?

Three reasons matter in practice:

1. Your editor becomes smarter. When it knows what type a variable holds, it can autocomplete methods accurately. Type user. and it shows you the methods that actually exist on a User object, not a generic list of possibilities.

2. Bugs get caught earlier. A type checker finds mistakes like "you're passing a string where an int is expected" before you run the code. No more crashes when a customer hits that rarely-used code path.

3. The code documents itself. When you see process_users(users: list[User]) -> dict[str, int], you immediately know: this function takes a list of User objects and returns a dictionary mapping strings to integers. No need to read the implementation to guess.

Basic type annotations

The simplest annotations are for Python's built-in types:

name: str = "Scipio"
age: int = 42
price: float = 99.99
active: bool = True

For function returns, use -> None when the function doesn't return anything:

def log_message(message: str) -> None:
    print(f"LOG: {message}")

This says "this function returns nothing useful." It might technically return None, but the hint tells readers: don't expect a return value.

Collections: what's inside matters

When you annotate a list or dictionary, you specify what type of elements it contains. This is where type hints get powerful, because you're not just saying "this is a list" - you're documenting what kind of data the list holds.

Think about it: in Python, a list can contain anything. Integers, strings, dictionaries, other lists, mixed types - anything. But in practice, your lists usually hold one specific type of thing. A list of user IDs (integers), a list of names (strings), a list of config objects. The type system lets you capture that intent:

numbers: list[int] = [1, 2, 3]

This says: numbers is a list, and every element in that list is an int. Now your type checker can warn you if you accidentally add a string to it.

For dictionaries, specify both key and value types:

ages: dict[str, int] = {"Alice": 30, "Bob": 25}

String keys, integer values. If you try to do ages["Carol"] = "thirty", the type checker catches it.

Nota bene: Before Python 3.9, you had to import these from the typing module (from typing import List, Dict). In modern Python, just use the built-in list and dict with square brackets.

Optional: "maybe there's a value, maybe not"

One of the most common patterns in Python: a function that might return a value, or might return None.

def find_user(user_id: int) -> dict | None:
    users = {1: {"name": "Scipio"}, 2: {"name": "Marcus"}}
    return users.get(user_id)

The return type dict | None means: you'll get either a dictionary, or None. The pipe symbol | means "or". This warns anyone calling the function: check if the result is None before using it!

In older Python (before 3.10), you'd write Optional[dict] instead. It means the same thing:

from typing import Optional

def find_user(user_id: int) -> Optional[dict]:
    ...

The mental model: Optional doesn't mean "this is optional to provide." It means "this value might optionally be None instead of the stated type."

Union: "one of these types"

Sometimes a value can legitimately be one of several types. Use Union (or the | syntax in modern Python):

def format_value(value: int | str) -> str:
    if isinstance(value, int):
        return f"Number: {value}"
    return f"Text: {value}"

This says: value can be an int or a string, nothing else. The function handles both cases and always returns a string.

Dataclasses: structure with types

Python 3.7 introduced dataclasses, which work beautifully with type hints. Think of them as a clean way to define data structures without writing boilerplate.

The problem dataclasses solve: you often need simple classes that just hold data. A User with a name, email, and age. A Point with x and y coordinates. A Config with various settings. Writing these by hand means typing out __init__ methods, __repr__ for debugging, __eq__ for comparisons - all repetitive code that's identical across every data class you write.

Before dataclasses, you'd write a class like this:

class User:
    def __init__(self, name, email, age):
        self.name = name
        self.email = email
        self.age = age

With a dataclass and type hints, it's much cleaner:

from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str
    age: int

The @dataclass decorator generates __init__, __repr__, __eq__, and other methods automatically based on your annotations. You get clean data structures with minimal code.

Creating instances looks exactly like before:

user = User("Scipio", "[email protected]", 42)
print(user.name)

But now your type checker knows what fields exist and what types they hold.

Default values in dataclasses

You can provide default values for fields:

@dataclass
class Config:
    debug: bool = False
    timeout: int = 30

Nota bene: For mutable defaults (like lists), use field(default_factory=list) instead of = []. This avoids the classic mutable default argument trap where all instances share the same list object:

from dataclasses import dataclass, field

@dataclass
class Team:
    name: str
    members: list[str] = field(default_factory=list)

Static type checking with mypy

Type hints on their own don't do anything. They're just annotations. To actually check them, use a static type checker like mypy.

Install it with pip install mypy, then run it on your Python files:

mypy yourfile.py

If you write this code:

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

result = add(5, "10")

mypy will tell you: "Argument 2 has incompatible type 'str'; expected 'int'." Before you even run the code, you know there's a problem.

This is the real power of type hints: catching bugs at check-time instead of runtime.

So what did we actually cover?

In this episode, we covered the conceptual foundations of type hints in Python:

  • Why Python added type hints even though it's a dynamically typed language
  • How to think about type hints as documentation that tools verify
  • Basic annotations for variables, parameters, and return values
  • Collection types that specify what's inside (list[int], dict[str, int])
  • Optional and Union for values that can be multiple types or None
  • Dataclasses for clean data structures with built-in type annotations
  • Using mypy to check your type hints before runtime

Type hints aren't about making Python statically typed. They're about giving you and your tools more information to catch mistakes earlier. In small scripts, you might not need them. In larger codebases, they become invaluable.

Hope you learned something useful. Thanks for your time!

@scipio

Sort:  

Congratulations @scipio! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)

You published more than 90 posts.
Your next target is to reach 100 posts.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!

Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).

Consider setting @stemsocial as a beneficiary of this post's rewards if you would like to support the community and contribute to its mission of promoting science and education on Hive.