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 type checking with mypy! ๐ In this guide, weโll explore how to catch bugs before they bite by using Pythonโs most popular static type checker.
Youโll discover how mypy can transform your Python development experience by finding errors at development time instead of runtime. Whether youโre building web APIs ๐, data pipelines ๐, or command-line tools ๐ ๏ธ, understanding mypy is essential for writing robust, maintainable code.
By the end of this tutorial, youโll feel confident using mypy in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Type Checking with Mypy
๐ค What is Mypy?
Mypy is like having a helpful friend who reads your code before you run it ๐ต๏ธโโ๏ธ. Think of it as a spell-checker for your Python code that catches type-related mistakes before they cause problems in production!
In Python terms, mypy performs static type checking - analyzing your code without running it. This means you can:
- โจ Catch type errors before runtime
- ๐ Get better IDE support and autocomplete
- ๐ก๏ธ Make refactoring safer and easier
๐ก Why Use Mypy?
Hereโs why developers love mypy:
- Early Bug Detection ๐: Find errors during development
- Better Documentation ๐: Types serve as inline documentation
- Confident Refactoring ๐ง: Change code without fear
- Team Collaboration ๐ค: Clear interfaces between components
Real-world example: Imagine building an e-commerce API ๐. With mypy, you can ensure that price calculations always use the right data types, preventing those nasty โTypeError: unsupported operand type(s)โ in production!
๐ง Basic Syntax and Usage
๐ Installing and Running Mypy
Letโs start with the basics:
# ๐ First, install mypy!
# pip install mypy
# ๐จ Create a simple Python file: hello.py
def greet(name: str) -> str:
"""Say hello to someone! ๐"""
return f"Hello, {name}! Welcome to type checking!"
# ๐ฏ This will cause a type error
result = greet(123) # ๐ฅ Oops! We're passing a number instead of a string
print(result)
Now letโs run mypy:
# ๐ Run mypy on your file
mypy hello.py
# ๐ Mypy output:
# hello.py:7: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
๐ก Explanation: Mypy caught the error before we even ran the code! It noticed weโre passing an integer to a function expecting a string.
๐ฏ Common Mypy Commands
Here are the most useful mypy commands:
# ๐๏ธ Check a single file
mypy script.py
# ๐ฆ Check an entire package
mypy my_package/
# ๐จ Check with strict mode (catch more issues)
mypy --strict script.py
# ๐ Show error codes (helpful for suppressing specific warnings)
mypy --show-error-codes script.py
# ๐ Generate an HTML report
mypy --html-report ./mypy_report script.py
๐ก Practical Examples
๐ Example 1: Type-Safe Shopping Cart
Letโs build a shopping cart with proper type checking:
# ๐๏ธ shopping_cart.py
from typing import List, Dict, Optional
from decimal import Decimal
class Product:
"""A product in our store ๐ช"""
def __init__(self, id: str, name: str, price: Decimal, emoji: str) -> None:
self.id = id
self.name = name
self.price = price
self.emoji = emoji # Every product needs an emoji!
class ShoppingCart:
"""Type-safe shopping cart ๐"""
def __init__(self) -> None:
self.items: List[Product] = []
self.discount_code: Optional[str] = None
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) -> bool:
"""Apply discount code ๐๏ธ"""
valid_codes = ["SAVE10", "PYTHON20", "MYPY15"]
if code in valid_codes:
self.discount_code = code
print(f"โ
Discount code {code} applied!")
return True
print(f"โ Invalid discount code: {code}")
return False
def calculate_total(self) -> Decimal:
"""Calculate total with type safety ๐ฐ"""
subtotal = sum(item.price for item in self.items)
# Apply discount if exists
if self.discount_code == "SAVE10":
return subtotal * Decimal("0.9")
elif self.discount_code == "PYTHON20":
return subtotal * Decimal("0.8")
elif self.discount_code == "MYPY15":
return subtotal * Decimal("0.85")
return subtotal
# ๐ฎ Let's test our type-safe cart!
cart = ShoppingCart()
book = Product("1", "Python Type Checking Guide", Decimal("29.99"), "๐")
coffee = Product("2", "Developer Coffee", Decimal("4.99"), "โ")
cart.add_item(book)
cart.add_item(coffee)
# โ This would be caught by mypy:
# cart.add_item("not a product") # Type error!
# โ
This is type-safe:
cart.apply_discount("MYPY15")
total = cart.calculate_total()
print(f"Total: ${total:.2f} ๐ณ")
Run mypy to check:
mypy shopping_cart.py
# Success: no issues found in 1 source file โ
๐ฎ Example 2: Game Score Validator
Letโs create a type-safe game scoring system:
# ๐ game_scores.py
from typing import Dict, List, Literal, TypedDict, Union
from datetime import datetime
# ๐ฏ Define strict types for our game
GameMode = Literal["easy", "medium", "hard", "extreme"]
PlayerLevel = Literal[1, 2, 3, 4, 5]
class ScoreEntry(TypedDict):
"""Type-safe score entry ๐"""
player_name: str
score: int
level: PlayerLevel
mode: GameMode
timestamp: datetime
achievements: List[str]
class GameScoreValidator:
"""Validates and manages game scores ๐ฎ"""
def __init__(self) -> None:
self.high_scores: Dict[GameMode, List[ScoreEntry]] = {
"easy": [],
"medium": [],
"hard": [],
"extreme": []
}
def validate_score(self, entry: ScoreEntry) -> Union[str, None]:
"""Validate a score entry ๐"""
# Type checking ensures these checks are comprehensive
if entry["score"] < 0:
return "โ Score cannot be negative!"
if entry["score"] > 1_000_000:
return "โ Score seems unrealistic (>1M)"
if len(entry["player_name"]) < 3:
return "โ Player name too short"
if entry["level"] < 1 or entry["level"] > 5:
return "โ Invalid player level"
return None # โ
Valid!
def add_score(self, entry: ScoreEntry) -> bool:
"""Add a validated score ๐"""
error = self.validate_score(entry)
if error:
print(error)
return False
# Type system ensures mode is valid
self.high_scores[entry["mode"]].append(entry)
# Sort by score (highest first)
self.high_scores[entry["mode"]].sort(
key=lambda x: x["score"],
reverse=True
)
print(f"โ
Score added for {entry['player_name']}!")
return True
def get_top_scores(self, mode: GameMode, limit: int = 5) -> List[ScoreEntry]:
"""Get top scores for a game mode ๐"""
return self.high_scores[mode][:limit]
# ๐ฎ Test our type-safe game system
validator = GameScoreValidator()
# โ
Valid score entry
good_score: ScoreEntry = {
"player_name": "PythonMaster",
"score": 42000,
"level": 3,
"mode": "medium",
"timestamp": datetime.now(),
"achievements": ["๐ First Victory", "๐ Combo Master"]
}
validator.add_score(good_score)
# โ This would be caught by mypy:
# bad_score: ScoreEntry = {
# "player_name": "Cheater",
# "score": "not a number", # Type error!
# "level": 99, # Invalid level!
# "mode": "super_easy", # Invalid mode!
# "timestamp": "yesterday", # Wrong type!
# "achievements": "just one" # Should be a list!
# }
๐ Advanced Concepts
๐งโโ๏ธ Configuring Mypy with mypy.ini
When youโre ready to level up, create a configuration file:
# mypy.ini
[mypy]
# ๐ฏ Enable strict mode gradually
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
strict_equality = True
# ๐จ Pretty output
pretty = True
show_error_codes = True
show_error_context = True
# ๐ฆ Ignore missing imports for third-party libraries
[mypy-requests.*]
ignore_missing_imports = True
[mypy-pandas.*]
ignore_missing_imports = True
๐๏ธ Gradual Typing Strategy
For existing projects, adopt mypy gradually:
# ๐ gradual_typing.py
from typing import Any, cast
# Phase 1: Start with Any for complex types
def legacy_function(data: Any) -> Any:
"""Old function we're gradually typing ๐"""
# TODO: Add proper types later
return data["result"]
# Phase 2: Add basic types
def improved_function(data: dict[str, Any]) -> str:
"""Better typed version ๐"""
return str(data.get("result", ""))
# Phase 3: Full typing with validation
from typing import TypedDict
class DataPayload(TypedDict):
result: str
status: int
timestamp: float
def fully_typed_function(data: DataPayload) -> str:
"""Fully typed with validation! ๐"""
if data["status"] != 200:
raise ValueError(f"Bad status: {data['status']}")
return data["result"]
# ๐ก๏ธ Use cast when you know better than mypy
external_data = {"result": "success", "status": 200, "timestamp": 1234567890.0}
typed_data = cast(DataPayload, external_data)
result = fully_typed_function(typed_data)
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: The โAnyโ Escape Hatch
# โ Wrong way - bypassing type safety!
from typing import Any
def process_data(data: Any) -> Any:
"""This could hide so many bugs! ๐ฐ"""
return data.do_something() # No type checking here!
# โ
Correct way - be specific!
from typing import Protocol
class Processable(Protocol):
"""Define what we expect ๐ฏ"""
def do_something(self) -> str: ...
def process_data(data: Processable) -> str:
"""Now mypy helps us! ๐ก๏ธ"""
return data.do_something()
๐คฏ Pitfall 2: Ignoring Optional Types
# โ 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 None properly!
from typing import Optional
def get_user_age(user_id: str) -> Optional[int]:
user = find_user(user_id)
if user is None:
print(f"โ ๏ธ User {user_id} not found!")
return None
return user.age # โ
Safe now!
๐ ๏ธ Best Practices
- ๐ฏ Start Gradually: Use
--follow-imports=skip
initially - ๐ Type Public APIs First: Focus on function signatures
- ๐ก๏ธ Enable Strict Mode Progressively: One flag at a time
- ๐จ Use Type Aliases: Make complex types readable
- โจ Leverage Protocols: For duck typing with safety
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe Task Manager
Create a task management system with full type checking:
๐ Requirements:
- โ Tasks with title, status, priority, and due date
- ๐ท๏ธ Categories with emoji identifiers
- ๐ค User assignment with role validation
- ๐ Statistics calculation with type safety
- ๐จ Each task must have an emoji status indicator!
๐ Bonus Points:
- Add task dependencies
- Implement deadline warnings
- Create a type-safe query system
๐ก Solution
๐ Click to see solution
# ๐ฏ Type-safe task manager!
from typing import Dict, List, Optional, Literal, TypedDict
from datetime import datetime, timedelta
from enum import Enum
# ๐จ Define our type system
TaskStatus = Literal["pending", "in_progress", "completed", "cancelled"]
Priority = Literal["low", "medium", "high", "urgent"]
UserRole = Literal["viewer", "contributor", "manager", "admin"]
class TaskEmoji(Enum):
"""Status emojis ๐จ"""
PENDING = "โณ"
IN_PROGRESS = "๐"
COMPLETED = "โ
"
CANCELLED = "โ"
class Category(TypedDict):
"""Task category with emoji ๐ท๏ธ"""
name: str
emoji: str
color: str
class User(TypedDict):
"""User with role ๐ค"""
id: str
name: str
role: UserRole
class Task(TypedDict):
"""Complete task definition ๐"""
id: str
title: str
description: str
status: TaskStatus
priority: Priority
category: Category
assignee: Optional[User]
due_date: Optional[datetime]
created_at: datetime
dependencies: List[str] # Task IDs
class TaskManager:
"""Type-safe task management system ๐ฏ"""
def __init__(self) -> None:
self.tasks: Dict[str, Task] = {}
self.task_counter: int = 0
def create_task(
self,
title: str,
description: str,
priority: Priority,
category: Category,
assignee: Optional[User] = None,
due_date: Optional[datetime] = None,
dependencies: Optional[List[str]] = None
) -> Task:
"""Create a new task with validation ๐"""
# Validate assignee permissions
if assignee and assignee["role"] == "viewer":
raise ValueError("โ Viewers cannot be assigned tasks!")
# Validate dependencies exist
if dependencies:
for dep_id in dependencies:
if dep_id not in self.tasks:
raise ValueError(f"โ Dependency {dep_id} not found!")
self.task_counter += 1
task_id = f"TASK-{self.task_counter:04d}"
task: Task = {
"id": task_id,
"title": title,
"description": description,
"status": "pending",
"priority": priority,
"category": category,
"assignee": assignee,
"due_date": due_date,
"created_at": datetime.now(),
"dependencies": dependencies or []
}
self.tasks[task_id] = task
print(f"โ
Created task {task_id}: {title}")
return task
def update_status(self, task_id: str, new_status: TaskStatus) -> bool:
"""Update task status with validation ๐"""
if task_id not in self.tasks:
print(f"โ Task {task_id} not found!")
return False
task = self.tasks[task_id]
# Check dependencies
if new_status == "completed":
for dep_id in task["dependencies"]:
dep_task = self.tasks.get(dep_id)
if dep_task and dep_task["status"] != "completed":
print(f"โ Cannot complete: dependency {dep_id} not done!")
return False
old_status = task["status"]
task["status"] = new_status
# Get emoji for new status
emoji = TaskEmoji[new_status.upper()].value
print(f"{emoji} Task {task_id}: {old_status} โ {new_status}")
return True
def get_deadline_warnings(self) -> List[Task]:
"""Get tasks due soon โฐ"""
warnings: List[Task] = []
now = datetime.now()
warning_threshold = now + timedelta(days=2)
for task in self.tasks.values():
if (task["status"] != "completed" and
task["due_date"] and
task["due_date"] <= warning_threshold):
warnings.append(task)
return sorted(warnings, key=lambda t: t["due_date"] or now)
def get_statistics(self) -> Dict[str, int]:
"""Calculate task statistics ๐"""
stats: Dict[str, int] = {
"total": len(self.tasks),
"pending": 0,
"in_progress": 0,
"completed": 0,
"cancelled": 0,
"overdue": 0
}
now = datetime.now()
for task in self.tasks.values():
stats[task["status"]] += 1
if (task["due_date"] and
task["due_date"] < now and
task["status"] not in ["completed", "cancelled"]):
stats["overdue"] += 1
return stats
# ๐ฎ Test it out!
manager = TaskManager()
# Create categories
dev_category: Category = {"name": "Development", "emoji": "๐ป", "color": "blue"}
bug_category: Category = {"name": "Bug Fix", "emoji": "๐", "color": "red"}
# Create users
alice: User = {"id": "alice", "name": "Alice", "role": "manager"}
bob: User = {"id": "bob", "name": "Bob", "role": "contributor"}
# Create tasks
task1 = manager.create_task(
title="Setup mypy configuration",
description="Configure mypy for the project",
priority="high",
category=dev_category,
assignee=alice,
due_date=datetime.now() + timedelta(days=1)
)
task2 = manager.create_task(
title="Fix type errors",
description="Fix all mypy errors in codebase",
priority="urgent",
category=bug_category,
assignee=bob,
dependencies=[task1["id"]]
)
# Update status
manager.update_status(task1["id"], "completed")
manager.update_status(task2["id"], "in_progress")
# Check warnings
warnings = manager.get_deadline_warnings()
if warnings:
print(f"โ ๏ธ {len(warnings)} tasks due soon!")
# Get stats
stats = manager.get_statistics()
print(f"๐ Stats: {stats}")
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Install and run mypy with confidence ๐ช
- โ Configure mypy for your projects ๐ก๏ธ
- โ Write type-safe Python code that catches bugs early ๐ฏ
- โ Debug type errors like a pro ๐
- โ Gradually adopt typing in existing codebases! ๐
Remember: Type checking is your friend, not your enemy! Itโs here to help you write better, more maintainable code. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered running mypy for type checking!
Hereโs what to do next:
- ๐ป Install mypy and try it on your own projects
- ๐๏ธ Start with basic type annotations and gradually increase strictness
- ๐ Move on to our next tutorial on advanced type annotations
- ๐ Share your type-safe code with the Python community!
Remember: Every Python expert started without types. Adding them is a journey, not a destination. Keep coding, keep learning, and most importantly, have fun with type safety! ๐
Happy type checking! ๐๐โจ