Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Mypy and static type checking in Python! ๐ In this guide, weโll explore how to add type safety to your Python projects and catch bugs before they bite!
Youโll discover how Mypy can transform your Python development experience. Whether youโre building web applications ๐, data pipelines ๐, or machine learning models ๐ค, understanding static type checking is essential for writing robust, maintainable code.
By the end of this tutorial, youโll feel confident using Mypy to make your Python code safer and more professional! Letโs dive in! ๐โโ๏ธ
๐ Understanding Mypy and Static Type Checking
๐ค What is Mypy?
Mypy is like a spell-checker for your Python codeโs types! ๐ช Think of it as a helpful assistant that reads your code before it runs and tells you โHey, youโre trying to add a number to a string here - that wonโt work!โ
In Python terms, Mypy is a static type checker that analyzes your code without executing it. This means you can:
- โจ Catch type-related bugs early
- ๐ Get better IDE support with autocomplete
- ๐ก๏ธ Make your code self-documenting
- ๐ Improve code readability for your team
๐ก Why Use Mypy?
Hereโs why developers love static type checking:
- Early Bug Detection ๐: Find errors at development time, not runtime
- Better Refactoring ๐ง: Change code with confidence
- Code Documentation ๐: Types serve as inline documentation
- IDE Support ๐ป: Enhanced autocomplete and suggestions
Real-world example: Imagine building an e-commerce system ๐. With Mypy, you can ensure that price calculations always use numbers, not accidentally mix strings!
๐ง Basic Syntax and Usage
๐ Installing and Using Mypy
Letโs start with getting Mypy set up:
# ๐ First, install mypy!
# pip install mypy
# ๐จ Create a simple typed Python file
def greet(name: str) -> str:
"""Say hello to someone! ๐"""
return f"Hello, {name}! ๐"
def calculate_total(price: float, quantity: int) -> float:
"""Calculate the total cost ๐ฐ"""
return price * quantity
# ๐ฏ Type annotations for variables
user_name: str = "Alice"
item_price: float = 29.99
item_count: int = 3
# โจ Using our typed functions
greeting = greet(user_name)
total = calculate_total(item_price, item_count)
print(f"{greeting} Your total is: ${total:.2f}")
To check this file with Mypy:
# ๐ Run mypy on your file
mypy your_file.py
๐ฏ Common Type Patterns
Here are patterns youโll use daily:
from typing import List, Dict, Optional, Union, Tuple
# ๐๏ธ Pattern 1: Lists and collections
def process_items(items: List[str]) -> List[str]:
"""Process a list of items ๐ฆ"""
return [item.upper() for item in items]
# ๐จ Pattern 2: Dictionaries
def create_user(name: str, age: int) -> Dict[str, Union[str, int]]:
"""Create a user profile ๐ค"""
return {
"name": name,
"age": age,
"emoji": "๐"
}
# ๐ Pattern 3: Optional values
def find_item(items: List[str], target: str) -> Optional[int]:
"""Find an item's index, or None if not found ๐"""
try:
return items.index(target)
except ValueError:
return None
# ๐ฏ Pattern 4: Tuples for multiple returns
def get_stats(numbers: List[float]) -> Tuple[float, float]:
"""Get min and max values ๐"""
return min(numbers), max(numbers)
๐ก Practical Examples
๐ Example 1: Type-Safe Shopping Cart
Letโs build a shopping cart with proper types:
from typing import List, Dict, Optional
from dataclasses import dataclass
from datetime import datetime
# ๐๏ธ Define our product type
@dataclass
class Product:
id: str
name: str
price: float
emoji: str # Every product needs an emoji!
category: str
# ๐ Shopping cart with type safety
class ShoppingCart:
def __init__(self) -> None:
self.items: List[Product] = []
self.discounts: Dict[str, float] = {}
def add_item(self, product: Product) -> None:
"""Add item to cart โ"""
self.items.append(product)
print(f"Added {product.emoji} {product.name} to cart!")
def apply_discount(self, code: str, percentage: float) -> bool:
"""Apply a discount code ๐๏ธ"""
if 0 <= percentage <= 100:
self.discounts[code] = percentage / 100
print(f"โ
Applied {percentage}% discount!")
return True
print(f"โ Invalid discount percentage!")
return False
def calculate_total(self) -> float:
"""Calculate total with discounts ๐ฐ"""
subtotal = sum(item.price for item in self.items)
discount_amount = sum(subtotal * disc for disc in self.discounts.values())
return subtotal - discount_amount
def get_summary(self) -> Dict[str, Union[int, float, List[str]]]:
"""Get cart summary ๐"""
return {
"item_count": len(self.items),
"total": self.calculate_total(),
"items": [f"{item.emoji} {item.name}" for item in self.items]
}
# ๐ฎ Let's use it with type safety!
cart = ShoppingCart()
# โ
This works - correct types
book = Product("1", "Python Book", 29.99, "๐", "books")
coffee = Product("2", "Coffee", 4.99, "โ", "beverages")
cart.add_item(book)
cart.add_item(coffee)
cart.apply_discount("SAVE10", 10)
# โ Mypy would catch these errors!
# cart.add_item("not a product") # Type error!
# cart.apply_discount("INVALID", "twenty") # Type error!
๐ฎ Example 2: Type-Safe Game System
Letโs create a game with proper type checking:
from typing import List, Dict, Optional, Protocol
from enum import Enum
from abc import ABC, abstractmethod
# ๐ฏ Define game types
class PlayerClass(Enum):
WARRIOR = "warrior"
MAGE = "mage"
ROGUE = "rogue"
class ItemType(Enum):
WEAPON = "weapon"
ARMOR = "armor"
POTION = "potion"
# ๐ฎ Item protocol
class GameItem(Protocol):
name: str
value: int
emoji: str
item_type: ItemType
@dataclass
class Weapon:
name: str
value: int
emoji: str
item_type: ItemType = ItemType.WEAPON
damage: int = 10
@dataclass
class Armor:
name: str
value: int
emoji: str
item_type: ItemType = ItemType.ARMOR
defense: int = 5
# ๐ค Player class with types
class Player:
def __init__(self, name: str, player_class: PlayerClass) -> None:
self.name = name
self.player_class = player_class
self.level: int = 1
self.health: int = 100
self.inventory: List[GameItem] = []
self.achievements: List[str] = ["๐ First Steps"]
def add_item(self, item: GameItem) -> bool:
"""Add item to inventory ๐"""
if len(self.inventory) < 20: # Inventory limit
self.inventory.append(item)
print(f"โจ {self.name} found {item.emoji} {item.name}!")
return True
print(f"โ Inventory full!")
return False
def use_potion(self, heal_amount: int) -> None:
"""Use a healing potion ๐งช"""
old_health = self.health
self.health = min(100, self.health + heal_amount)
healed = self.health - old_health
print(f"๐ Healed for {healed} HP! Health: {self.health}/100")
def level_up(self) -> None:
"""Level up the player ๐"""
self.level += 1
self.health = 100 # Full heal on level up
self.achievements.append(f"๐ Level {self.level} {self.player_class.value.title()}")
print(f"๐ {self.name} reached level {self.level}!")
def get_stats(self) -> Dict[str, Union[str, int, List[str]]]:
"""Get player statistics ๐"""
return {
"name": self.name,
"class": self.player_class.value,
"level": self.level,
"health": self.health,
"items": len(self.inventory),
"achievements": self.achievements
}
# ๐ฎ Using the type-safe game system
player = Player("Hero", PlayerClass.WARRIOR)
sword = Weapon("Iron Sword", 50, "โ๏ธ", damage=15)
shield = Armor("Wooden Shield", 30, "๐ก๏ธ", defense=8)
player.add_item(sword)
player.add_item(shield)
player.use_potion(25)
player.level_up()
print(f"๐ Stats: {player.get_stats()}")
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Generics and Type Variables
When youโre ready to level up, try generic types:
from typing import TypeVar, Generic, List, Callable, Optional
# ๐ฏ Create a generic type variable
T = TypeVar('T')
# ๐ช Generic container class
class MagicBox(Generic[T]):
def __init__(self) -> None:
self._items: List[T] = []
self.sparkles = "โจ"
def add(self, item: T) -> None:
"""Add an item to the magic box ๐ฆ"""
self._items.append(item)
print(f"{self.sparkles} Added item to magic box!")
def get_all(self) -> List[T]:
"""Get all items from the box ๐"""
return self._items.copy()
def transform(self, func: Callable[[T], T]) -> None:
"""Transform all items with magic! ๐ช"""
self._items = [func(item) for item in self._items]
# ๐ฎ Using generic types
number_box: MagicBox[int] = MagicBox()
number_box.add(42)
number_box.add(7)
number_box.transform(lambda x: x * 2)
string_box: MagicBox[str] = MagicBox()
string_box.add("Hello")
string_box.add("World")
string_box.transform(lambda s: s.upper())
๐๏ธ Advanced Topic 2: Protocols and Structural Subtyping
For the brave developers, hereโs structural subtyping:
from typing import Protocol, runtime_checkable
# ๐ Define a protocol
@runtime_checkable
class Drawable(Protocol):
"""Anything that can be drawn ๐จ"""
def draw(self) -> str: ...
@property
def color(self) -> str: ...
# ๐ฏ Classes that implement the protocol
class Circle:
def __init__(self, radius: float, color: str) -> None:
self.radius = radius
self._color = color
def draw(self) -> str:
return f"โญ Drawing a {self._color} circle!"
@property
def color(self) -> str:
return self._color
class Square:
def __init__(self, size: float, color: str) -> None:
self.size = size
self._color = color
def draw(self) -> str:
return f"โฌ Drawing a {self._color} square!"
@property
def color(self) -> str:
return self._color
# ๐จ Function that accepts any drawable
def paint_shape(shape: Drawable) -> None:
"""Paint any drawable shape ๐๏ธ"""
print(shape.draw())
print(f"Using {shape.color} paint ๐จ")
# โจ Both work with the protocol!
circle = Circle(5.0, "red")
square = Square(10.0, "blue")
paint_shape(circle) # โ
Works!
paint_shape(square) # โ
Also works!
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: The โAnyโ Escape Hatch
from typing import Any, List
# โ Wrong way - losing all type safety!
def process_data(data: Any) -> Any:
"""This could be anything ๐ฐ"""
return data.do_something() # No type checking!
# โ
Correct way - be specific!
def process_numbers(data: List[int]) -> int:
"""Process a list of numbers safely ๐ก๏ธ"""
return sum(data) # Type checker ensures this works!
๐คฏ Pitfall 2: Ignoring Optional Types
from typing import Optional
# โ Dangerous - might be None!
def get_user_age(user_id: str) -> int:
user = find_user(user_id) # Could return None!
return user.age # ๐ฅ AttributeError if user is None!
# โ
Safe - handle the None case!
def get_user_age(user_id: str) -> Optional[int]:
user = find_user(user_id)
if user is None:
print("โ ๏ธ User not found!")
return None
return user.age # โ
Safe now!
๐ต Pitfall 3: Mutable Default Arguments
from typing import List, Optional
# โ Classic Python gotcha!
def add_item(item: str, items: List[str] = []) -> List[str]:
items.append(item) # ๐ฅ Shared mutable default!
return items
# โ
Correct way - use None as default!
def add_item(item: str, items: Optional[List[str]] = None) -> List[str]:
if items is None:
items = [] # โจ Fresh list each time!
items.append(item)
return items
๐ ๏ธ Best Practices
- ๐ฏ Start Gradually: Add types to new code first, then existing code
- ๐ Use Type Aliases: Make complex types readable
- ๐ก๏ธ Enable Strict Mode: Use
--strict
flag for maximum safety - ๐จ Document with Types: Let types tell the story
- โจ Keep It Simple: Donโt over-engineer your type annotations
from typing import Dict, List, Union
# ๐จ Type aliases for clarity
UserId = str
UserData = Dict[str, Union[str, int, List[str]]]
# โ
Clear and readable
def get_user_info(user_id: UserId) -> UserData:
"""Get user information by ID ๐ค"""
return {
"id": user_id,
"name": "Alice",
"age": 28,
"hobbies": ["coding", "gaming", "reading"]
}
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe Task Management System
Create a fully typed task management application:
๐ Requirements:
- โ Tasks with title, description, status, and priority
- ๐ท๏ธ Categories for tasks (work, personal, urgent)
- ๐ค User assignment with type safety
- ๐ Due dates with proper datetime types
- ๐จ Each task needs an emoji based on priority!
- ๐ Search functionality with type-safe filters
๐ Bonus Points:
- Add type-safe sorting by different fields
- Implement a notification system with protocols
- Create custom type guards for task validation
๐ก Solution
๐ Click to see solution
from typing import List, Dict, Optional, Protocol, TypedDict, Literal
from datetime import datetime
from enum import Enum
from dataclasses import dataclass, field
# ๐ฏ Define our type system
class Priority(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class Status(Enum):
TODO = "todo"
IN_PROGRESS = "in_progress"
DONE = "done"
class Category(Enum):
WORK = "work"
PERSONAL = "personal"
URGENT = "urgent"
# ๐ Task type with all fields
@dataclass
class Task:
title: str
description: str
status: Status = Status.TODO
priority: Priority = Priority.MEDIUM
category: Category = Category.PERSONAL
assignee: Optional[str] = None
due_date: Optional[datetime] = None
created_at: datetime = field(default_factory=datetime.now)
id: str = field(default_factory=lambda: str(datetime.now().timestamp()))
@property
def emoji(self) -> str:
"""Get emoji based on priority ๐จ"""
emoji_map = {
Priority.LOW: "๐ข",
Priority.MEDIUM: "๐ก",
Priority.HIGH: "๐ ",
Priority.URGENT: "๐ด"
}
return emoji_map[self.priority]
# ๐ Search filters type
class TaskFilter(TypedDict, total=False):
status: Optional[Status]
priority: Optional[Priority]
category: Optional[Category]
assignee: Optional[str]
# ๐ Task manager with full type safety
class TaskManager:
def __init__(self) -> None:
self.tasks: List[Task] = []
def add_task(self, task: Task) -> None:
"""Add a new task โ"""
self.tasks.append(task)
print(f"{task.emoji} Added: {task.title}")
def update_status(self, task_id: str, new_status: Status) -> bool:
"""Update task status ๐"""
for task in self.tasks:
if task.id == task_id:
old_status = task.status
task.status = new_status
print(f"โ
Updated {task.title}: {old_status.value} โ {new_status.value}")
return True
print(f"โ Task not found!")
return False
def search_tasks(self, **filters: TaskFilter) -> List[Task]:
"""Search tasks with type-safe filters ๐"""
results = self.tasks.copy()
for key, value in filters.items():
if value is not None:
results = [t for t in results if getattr(t, key) == value]
return results
def get_stats(self) -> Dict[str, int]:
"""Get task statistics ๐"""
stats = {
"total": len(self.tasks),
"todo": len([t for t in self.tasks if t.status == Status.TODO]),
"in_progress": len([t for t in self.tasks if t.status == Status.IN_PROGRESS]),
"done": len([t for t in self.tasks if t.status == Status.DONE]),
}
return stats
def get_overdue_tasks(self) -> List[Task]:
"""Get overdue tasks โฐ"""
now = datetime.now()
return [
task for task in self.tasks
if task.due_date and task.due_date < now and task.status != Status.DONE
]
# ๐ฎ Custom type guard
def is_urgent_task(task: Task) -> bool:
"""Type guard for urgent tasks ๐จ"""
return task.priority == Priority.URGENT or task.category == Category.URGENT
# ๐ฏ Test it out!
manager = TaskManager()
# Add some tasks
task1 = Task(
title="Learn Mypy",
description="Master static type checking",
priority=Priority.HIGH,
category=Category.PERSONAL
)
task2 = Task(
title="Code Review",
description="Review team's pull requests",
priority=Priority.URGENT,
category=Category.WORK,
assignee="Alice",
due_date=datetime(2024, 12, 31)
)
manager.add_task(task1)
manager.add_task(task2)
# Search with type safety
work_tasks = manager.search_tasks(category=Category.WORK)
urgent_tasks = [t for t in manager.tasks if is_urgent_task(t)]
print(f"\n๐ Stats: {manager.get_stats()}")
print(f"๐จ Urgent tasks: {len(urgent_tasks)}")
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Add type annotations to your Python code ๐ช
- โ Run Mypy to catch bugs early ๐ก๏ธ
- โ Use advanced types like Generics and Protocols ๐ฏ
- โ Avoid common pitfalls that trip up beginners ๐
- โ Build type-safe applications with confidence! ๐
Remember: Static typing in Python is optional but powerful! Start small and gradually add more types as you get comfortable. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Mypy and static type checking!
Hereโs what to do next:
- ๐ป Install Mypy and start adding types to your current project
- ๐๏ธ Build a small type-safe application using what you learned
- ๐ Configure Mypy with a
mypy.ini
file for your project - ๐ Explore more advanced features like type stubs and plugins
Remember: Every Python expert started somewhere. Keep practicing, keep learning, and most importantly, have fun making your code safer! ๐
Happy type checking! ๐๐โจ