Skip to main content

Milestones & Mini-Projects

Bringing the Syllabus to Life with Practical Workโ€‹

In short: one small, working project for each of the seven core milestones. Each project uses exactly the skills from that milestone of the Syllabus.

How to use this page
  1. Read the milestone in the Syllabus first.
  2. Create the project folder shown, and type the code โ€” don't paste it.
  3. Run it, then do the Try it tasks at the end of the milestone. They add tests and a new feature, which is where most of the learning happens.
  4. Tick the checklist and commit to git before moving on.

Milestone 1: Getting Started & Fundamentalsโ€‹

In short: set up a clean project and build a small, type-hinted settings class that can load JSON.

Mini-Project: Configuration Management Systemโ€‹

Goal: Setup professional Python environment and write type-hinted config handler

Project Structureโ€‹

week1_project/
โ”œโ”€โ”€ .venv/ # Virtual environment
โ”œโ”€โ”€ .gitignore # Python-specific
โ”œโ”€โ”€ pyproject.toml # Modern Python config
โ”œโ”€โ”€ README.md
โ””โ”€โ”€ src/
โ””โ”€โ”€ config.py # Main application

Implementation (config.py)โ€‹

"""Configuration management system with type hints."""
from typing import Optional, Dict, Any
from pathlib import Path
import json


class Config:
"""Application configuration handler."""

def __init__(self, debug: bool = False, timeout: int = 30,
api_url: str = "http://localhost:8000"):
self.debug: bool = debug
self.timeout: int = timeout
self.api_url: str = api_url
self._cache: Dict[str, Any] = {}

def get(self, key: str, default: Optional[Any] = None) -> Any:
"""Get configuration value."""
return self._cache.get(key, default)

def set(self, key: str, value: Any) -> None:
"""Set configuration value."""
self._cache[key] = value

def load_from_json(self, filepath: str) -> bool:
"""Load configuration from JSON file."""
try:
data = json.loads(Path(filepath).read_text())
for key, value in data.items():
self.set(key, value)
return True
except (FileNotFoundError, json.JSONDecodeError) as e:
if self.debug:
print(f"Error loading config: {e}")
return False

def is_production(self) -> bool:
"""Check if running in production."""
return not self.debug and self.timeout > 60


# Test it
if __name__ == "__main__":
config = Config(debug=True, timeout=30)
config.set("app_name", "MyApp")
print(f"App: {config.get('app_name')}")
print(f"Debug: {config.debug}")

Deliverables Checklistโ€‹

  • Virtual environment created and activated
  • Project structure setup (.venv, .gitignore, pyproject.toml)
  • Type-hinted Python class with methods
  • JSON loading functionality
  • README with setup instructions
  • Git initialized with first commit

Verification Stepsโ€‹

# Setup
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate

# Run
python src/config.py

# Verify types
pip install mypy
mypy src/config.py # Should pass type checking

Watch out forโ€‹

  • Mutable default arguments: never write def f(items=[]); use items=None and create the list inside
  • Type hints don't enforce types at runtime (tools like mypy needed)
  • __slots__ (a memory-saving option for classes) isn't needed here โ€” only optimise once you've measured a problem

Try itโ€‹

Write test_config.py with three pytest tests: get returns the default for a missing key, load_from_json returns False for a missing file, and is_production() is True for Config(debug=False, timeout=120).


Milestone 2: Collections & Data Structuresโ€‹

In short: turn a JSON API response (a dict containing a list of dicts) into useful answers with comprehensions.

Mini-Project: JSON API Response Parserโ€‹

Goal: Master dictionaries, lists, and comprehensions with real data

Project Structureโ€‹

week2_project/
โ”œโ”€โ”€ data/
โ”‚ โ””โ”€โ”€ sample_response.json # Real API response
โ”œโ”€โ”€ src/
โ”‚ โ””โ”€โ”€ parser.py # Your solution
โ””โ”€โ”€ test_parser.py # Unit tests

Sample Data (data/sample_response.json)โ€‹

{
"users": [
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"roles": ["admin", "user"],
"metadata": {
"created_at": "2024-01-01",
"login_count": 45
}
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com",
"roles": ["user"],
"metadata": {
"created_at": "2024-01-02",
"login_count": 12
}
}
]
}

Implementation (src/parser.py)โ€‹

"""Parse and transform API responses."""
import json
from pathlib import Path
from typing import List, Dict, Set


class APIResponseParser:
"""Parse JSON API responses."""

def __init__(self, filepath: str):
self.data = json.loads(Path(filepath).read_text())

def get_user_names(self) -> List[str]:
"""Extract list of user names."""
# Using list comprehension
return [user['name'] for user in self.data['users']]

def get_users_by_role(self) -> Dict[str, List[str]]:
"""Organize users by role."""
result = {}
for user in self.data['users']:
for role in user['roles']:
if role not in result:
result[role] = []
result[role].append(user['name'])
return result

def get_users_by_role_dict_comp(self) -> Dict[str, List[str]]:
"""Same as above, using dict comprehension."""
# More Pythonic
all_roles = {role for user in self.data['users']
for role in user['roles']}

return {
role: [u['name'] for u in self.data['users']
if role in u['roles']]
for role in all_roles
}

def get_admin_emails(self) -> List[str]:
"""Get emails of users with admin role."""
return [
user['email'] for user in self.data['users']
if 'admin' in user['roles']
]

def get_unique_roles(self) -> Set[str]:
"""Get all unique roles."""
# Using set comprehension
return {role for user in self.data['users']
for role in user['roles']}

def get_login_stats(self) -> Dict[str, int]:
"""Create dict of name -> login_count."""
# Using dict comprehension
return {
user['name']: user['metadata']['login_count']
for user in self.data['users']
}

def get_top_users(self, n: int = 1) -> List[Dict]:
"""Get n users with most logins."""
sorted_users = sorted(
self.data['users'],
key=lambda u: u['metadata']['login_count'],
reverse=True
)
return sorted_users[:n]


if __name__ == "__main__":
parser = APIResponseParser("data/sample_response.json")

print("User names:", parser.get_user_names())
print("Users by role:", parser.get_users_by_role())
print("Admin emails:", parser.get_admin_emails())
print("Unique roles:", parser.get_unique_roles())
print("Login stats:", parser.get_login_stats())
print("Top user:", parser.get_top_users(1))

Test File (test_parser.py)โ€‹

"""Test the parser."""
import pytest
from src.parser import APIResponseParser


@pytest.fixture
def parser():
return APIResponseParser("data/sample_response.json")


def test_get_user_names(parser):
names = parser.get_user_names()
assert len(names) == 2
assert "Alice" in names
assert "Bob" in names


def test_get_users_by_role(parser):
by_role = parser.get_users_by_role()
assert "admin" in by_role
assert "Alice" in by_role["admin"]
assert "Bob" not in by_role["admin"]


def test_get_admin_emails(parser):
emails = parser.get_admin_emails()
assert "alice@example.com" in emails
assert "bob@example.com" not in emails


def test_get_unique_roles(parser):
roles = parser.get_unique_roles()
assert roles == {"admin", "user"}


def test_get_login_stats(parser):
stats = parser.get_login_stats()
assert stats["Alice"] == 45
assert stats["Bob"] == 12


def test_get_top_users(parser):
top = parser.get_top_users(1)
assert top[0]['name'] == "Alice" # Alice has most logins

Deliverables Checklistโ€‹

  • Load and parse JSON file
  • Extract data using list comprehensions
  • Create lookup dicts using dict comprehensions
  • Create sets of unique values using set comprehensions
  • Sort complex data structures
  • Write unit tests for all methods
  • Run tests and verify all pass

Performance Exerciseโ€‹

# Which is faster? Create benchmark

import timeit

# Method 1: List + loop
def users_by_role_loop():
result = {}
for user in parser.data['users']:
for role in user['roles']:
if role not in result:
result[role] = []
result[role].append(user['name'])
return result

# Method 2: Dict comprehension
def users_by_role_comp():
return parser.get_users_by_role_dict_comp()

# Benchmark
t1 = timeit.timeit(users_by_role_loop, number=10000)
t2 = timeit.timeit(users_by_role_comp, number=10000)
print(f"Loop: {t1:.4f}s, Comprehension: {t2:.4f}s")
# Result: Comprehension is typically 2-3x faster

Try itโ€‹

Add a get_users_by_domain() method that returns a dict of email domain โ†’ list of names, and write a test for it.


Milestone 3: Control Flow & Logicโ€‹

In short: check and sort email addresses using if rules, guard clauses, and regex.

Mini-Project: Text-Based Email Validator & Classifierโ€‹

Goal: Master conditionals, guard clauses, and error handling

Implementation (email_validator.py)โ€‹

"""Email validation and classification system."""
import re
from typing import Tuple, Optional
from enum import Enum


class EmailType(Enum):
"""Email domain classifications."""
CORPORATE = "corporate"
PERSONAL = "personal"
UNKNOWN = "unknown"


class EmailValidator:
"""Validate and classify emails."""

# Pattern constants
PATTERN = re.compile(r'^[\w\.-]+@[\w\.-]+\.\w+$')
CORPORATE_DOMAINS = {"example.com", "company.org", "acme.co"}
PERSONAL_DOMAINS = {"gmail.com", "yahoo.com", "outlook.com"}

@staticmethod
def validate(email: str) -> Tuple[bool, Optional[str]]:
"""
Validate email format.

Returns:
(is_valid, error_message)
"""
# Guard clauses (return early for invalid cases)
if not email:
return False, "Email cannot be empty"

if not isinstance(email, str):
return False, "Email must be a string"

email = email.strip() # Remove whitespace

if len(email) > 255:
return False, "Email too long (max 255 chars)"

if EmailValidator.PATTERN.match(email):
return True, None

return False, "Invalid email format"

@staticmethod
def classify(email: str) -> EmailType:
"""Classify email by domain."""
# Validation first
is_valid, _ = EmailValidator.validate(email)
if not is_valid:
return EmailType.UNKNOWN

# Extract domain
domain = email.split('@')[1].lower()

# Classify with early returns
if domain in EmailValidator.CORPORATE_DOMAINS:
return EmailType.CORPORATE

if domain in EmailValidator.PERSONAL_DOMAINS:
return EmailType.PERSONAL

return EmailType.UNKNOWN

@staticmethod
def batch_validate(emails: list) -> dict:
"""Validate multiple emails and categorize results."""
results = {
'valid': [],
'invalid': [],
'corporate': [],
'personal': [],
'other': []
}

for email in emails:
is_valid, _ = EmailValidator.validate(email)

if not is_valid:
results['invalid'].append(email)
continue

results['valid'].append(email)

# Classify valid emails
email_type = EmailValidator.classify(email)

# Nested conditional with elif (avoid deeply nested code)
if email_type == EmailType.CORPORATE:
results['corporate'].append(email)
elif email_type == EmailType.PERSONAL:
results['personal'].append(email)
else:
results['other'].append(email)

return results


# Test cases
if __name__ == "__main__":
test_emails = [
"alice@example.com",
"bob@gmail.com",
"invalid-email",
"",
"charlie@company.org",
"diana@unknown.io"
]

print("=== Batch Validation ===")
results = EmailValidator.batch_validate(test_emails)
for category, emails in results.items():
print(f"{category}: {emails}")

print("\n=== Individual Tests ===")
test_cases = [
"test@example.com",
"invalid@",
"@example.com"
]

for email in test_cases:
is_valid, error = EmailValidator.validate(email)
if is_valid:
classification = EmailValidator.classify(email)
print(f"โœ“ {email} -> {classification.value}")
else:
print(f"โœ— {email} -> {error}")

Test Casesโ€‹

def test_valid_emails():
assert EmailValidator.validate("alice@example.com")[0] == True
assert EmailValidator.validate("test+tag@gmail.com")[0] == True

def test_invalid_emails():
assert EmailValidator.validate("")[0] == False
assert EmailValidator.validate("invalid-email")[0] == False
assert EmailValidator.validate("test@")[0] == False

def test_classification():
assert EmailValidator.classify("alice@example.com") == EmailType.CORPORATE
assert EmailValidator.classify("bob@gmail.com") == EmailType.PERSONAL
assert EmailValidator.classify("charlie@unknown.io") == EmailType.UNKNOWN

def test_guard_clauses():
"""Verify guard clauses prevent invalid processing."""
is_valid, error = EmailValidator.validate("")
assert error == "Email cannot be empty"
assert not is_valid

Key Concepts Practicedโ€‹

  • โœ“ Guard clauses (early returns)
  • โœ“ if/elif/else chains (avoiding deep nesting)
  • โœ“ Truthiness checks
  • โœ“ Error messages in returns
  • โœ“ Loop control (continue)
  • โœ“ List comprehensions in conditionals

Try itโ€‹

Add a rule that rejects emails longer than 254 characters, with a test for it. Then add one test case of your own for an edge case the page doesn't cover.


Milestone 4: Functions & Functional Programmingโ€‹

In short: build a rate limiter and a retry decorator โ€” functions that wrap other functions.

Mini-Project: Rate Limiter & Retry Handlerโ€‹

Goal: Master decorators, closures, and functional patterns

Implementation (rate_limiter.py)โ€‹

"""Rate limiting and retry mechanisms."""
import time
from functools import wraps
from typing import Callable, Any, TypeVar
from datetime import datetime, timedelta


T = TypeVar('T')


def rate_limit(max_calls: int, period_seconds: int) -> Callable:
"""
Decorator that rate limits function calls.

Args:
max_calls: Maximum calls allowed
period_seconds: Time period for rate limiting

Usage:
@rate_limit(max_calls=5, period_seconds=60)
def api_call():
pass
"""
def decorator(func: Callable[..., T]) -> Callable[..., T]:
calls = []

@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
now = datetime.now()

# Remove old calls outside the period
calls[:] = [call_time for call_time in calls
if now - call_time < timedelta(seconds=period_seconds)]

# Check if rate limit exceeded
if len(calls) >= max_calls:
raise Exception(f"Rate limit exceeded: {max_calls} calls per {period_seconds}s")

# Record this call
calls.append(now)

# Call the function
return func(*args, **kwargs)

return wrapper

return decorator


def retry(max_attempts: int = 3, backoff_factor: float = 2) -> Callable:
"""
Decorator that retries function on failure.

Args:
max_attempts: Maximum retry attempts
backoff_factor: Exponential backoff multiplier

Usage:
@retry(max_attempts=3, backoff_factor=2)
def flaky_function():
pass
"""
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
wait_time = 1 # Start with 1 second

for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
# Last attempt failed
print(f"Failed after {max_attempts} attempts: {e}")
raise

# Wait and retry
print(f"Attempt {attempt + 1} failed, retrying in {wait_time}s...")
time.sleep(wait_time)
wait_time *= backoff_factor

return wrapper

return decorator


# Closure-based rate limiter (alternative pattern)
def create_rate_limiter(max_calls: int, period_seconds: int):
"""
Factory function creating a reusable rate limiter.

Returns:
Function that checks if call is allowed
"""
calls = []

def is_allowed() -> bool:
"""Check if next call is allowed."""
now = datetime.now()

# Cleanup old calls
calls[:] = [t for t in calls
if now - t < timedelta(seconds=period_seconds)]

if len(calls) >= max_calls:
return False

calls.append(now)
return True

return is_allowed


# Test usage
if __name__ == "__main__":
print("=== Rate Limit Decorator ===")

@rate_limit(max_calls=3, period_seconds=5)
def api_call(endpoint: str):
print(f"Called {endpoint}")
return f"Response from {endpoint}"

# This works (first 3 calls)
for i in range(3):
result = api_call(f"/api/endpoint{i}")
print(f"Result: {result}")

# This fails (4th call exceeds limit)
try:
api_call("/api/endpoint3")
except Exception as e:
print(f"Error: {e}")

print("\n=== Retry Decorator ===")

attempt_count = 0

@retry(max_attempts=3, backoff_factor=1)
def flaky_function():
global attempt_count
attempt_count += 1
print(f"Attempt {attempt_count}")

if attempt_count < 2:
raise ConnectionError("Network unavailable")

return "Success"

result = flaky_function()
print(f"Final result: {result}")

print("\n=== Closure-based Rate Limiter ===")

limiter = create_rate_limiter(max_calls=2, period_seconds=10)

# First 2 calls allowed
print(f"Call 1: {limiter()}") # True
print(f"Call 2: {limiter()}") # True
print(f"Call 3: {limiter()}") # False

# Separate limiter instance
limiter2 = create_rate_limiter(max_calls=2, period_seconds=10)
print(f"Limiter2 Call 1: {limiter2()}") # True

Advanced: Decorator Stackingโ€‹

@retry(max_attempts=3)
@rate_limit(max_calls=5, period_seconds=60)
def robust_api_call(endpoint: str):
"""Combines both rate limiting and retry logic."""
return fetch_from_api(endpoint)

# Execution order: rate_limit -> retry -> actual function

Try itโ€‹

Write tests for the retry decorator: one where the function fails twice then succeeds, and one where it always fails. Use unittest.mock.Mock(side_effect=[...]) and patch time.sleep so the tests don't wait.


Milestone 5: Object-Oriented Programmingโ€‹

In short: model users and admins as classes that share code through inheritance.

Mini-Project: User Management System with Inheritanceโ€‹

Goal: Master OOP patterns and special methods

Implementation (user_system.py)โ€‹

"""User management system with OOP."""
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime
from enum import Enum


class UserRole(Enum):
"""User roles in the system."""
ADMIN = "admin"
MODERATOR = "moderator"
USER = "user"
GUEST = "guest"


class User:
"""Base user class."""

def __init__(self, id: int, username: str, email: str):
self.id = id
self.username = username
self.email = email
self.created_at = datetime.now()

def __str__(self) -> str:
return f"User({self.username})"

def __repr__(self) -> str:
return f"User(id={self.id}, username={self.username!r}, email={self.email!r})"

def __eq__(self, other) -> bool:
if not isinstance(other, User):
return NotImplemented
return self.id == other.id

def __lt__(self, other) -> bool:
"""Allow sorting users by username."""
if not isinstance(other, User):
return NotImplemented
return self.username < other.username

def __hash__(self) -> int:
"""Allow user objects in sets/dicts."""
return hash(self.id)

def get_info(self) -> str:
"""Get user information."""
return f"{self.username} <{self.email}>"


class AdminUser(User):
"""Admin user with special permissions."""

def __init__(self, id: int, username: str, email: str, admin_level: int = 1):
super().__init__(id, username, email)
self.admin_level = admin_level
self.deleted_users: List[int] = []

def delete_user(self, user_id: int) -> bool:
"""Admin can delete users."""
self.deleted_users.append(user_id)
return True

def ban_user(self, user_id: int, reason: str) -> bool:
"""Ban a user with reason."""
print(f"Admin {self.username} banned user {user_id}: {reason}")
return True

def get_info(self) -> str:
"""Override to include admin info."""
parent_info = super().get_info()
return f"{parent_info} [ADMIN Level {self.admin_level}]"


@dataclass
class ModeratorUser(User):
"""Moderator user (using dataclass)."""

moderated_users: List[int] = field(default_factory=list)
report_count: int = 0

def report_user(self, user_id: int) -> bool:
"""File a report on user."""
self.moderated_users.append(user_id)
self.report_count += 1
return True

def get_info(self) -> str:
return f"{self.username} (Moderator, {self.report_count} reports)"


class UserManager:
"""Manage user collection."""

def __init__(self):
self.users: dict[int, User] = {}
self._next_id = 1

def add_user(self, username: str, email: str, role: UserRole = UserRole.USER) -> User:
"""Create and add new user."""
user_id = self._next_id
self._next_id += 1

if role == UserRole.ADMIN:
user = AdminUser(user_id, username, email)
elif role == UserRole.MODERATOR:
user = ModeratorUser(user_id, username, email)
else:
user = User(user_id, username, email)

self.users[user_id] = user
return user

def get_user(self, user_id: int) -> Optional[User]:
"""Get user by ID."""
return self.users.get(user_id)

def get_all_users(self) -> List[User]:
"""Get all users, sorted by username."""
return sorted(self.users.values())

def get_admins(self) -> List[AdminUser]:
"""Get all admin users."""
return [u for u in self.users.values() if isinstance(u, AdminUser)]

def remove_user(self, user_id: int) -> bool:
"""Remove user."""
if user_id in self.users:
del self.users[user_id]
return True
return False


# Test it
if __name__ == "__main__":
manager = UserManager()

# Add users
alice = manager.add_user("alice", "alice@example.com", UserRole.ADMIN)
bob = manager.add_user("bob", "bob@example.com", UserRole.USER)
charlie = manager.add_user("charlie", "charlie@example.com", UserRole.MODERATOR)

print("=== All Users (sorted) ===")
for user in manager.get_all_users():
print(f" {user.get_info()}")

print("\n=== Admins ===")
for admin in manager.get_admins():
admin.ban_user(bob.id, "Spam")

print("\n=== String Representations ===")
print(f"str: {alice}")
print(f"repr: {alice!r}")

print("\n=== Equality & Hashing ===")
alice2 = User(alice.id, "alice", "alice@example.com")
print(f"alice == alice2: {alice == alice2}")

user_set = {alice, bob, charlie, alice2}
print(f"Set size (should be 3): {len(user_set)}") # alice and alice2 are same

Try itโ€‹

Add a Guest class that can't change anything, and write tests that check each role's permissions.


Milestone 6: Modules, Exceptions & Advanced Constructsโ€‹

In short: read a large log file line by line with generators, and handle bad lines safely.

Mini-Project: Log Parser with Generatorsโ€‹

Goal: Practice generators, context managers, and exceptions

Project Structureโ€‹

week6_project/
โ”œโ”€โ”€ logs/
โ”‚ โ””โ”€โ”€ app.log # Sample log file
โ”œโ”€โ”€ src/
โ”‚ โ”œโ”€โ”€ log_parser.py # Your solution
โ”‚ โ””โ”€โ”€ exceptions.py # Custom exceptions
โ””โ”€โ”€ test_parser.py # Tests

Implementation (src/exceptions.py)โ€‹

"""Custom exceptions for log parsing."""


class LogError(Exception):
"""Base exception for log parsing errors."""
pass


class InvalidLogFormat(LogError):
"""Raised when log line has invalid format."""
pass


class LogFileError(LogError):
"""Raised when log file cannot be read."""
pass

Implementation (src/log_parser.py)โ€‹

"""Parse log files efficiently with generators."""
import re
from pathlib import Path
from datetime import datetime
from typing import Generator, Dict, Optional
from dataclasses import dataclass
from .exceptions import InvalidLogFormat, LogFileError


@dataclass
class LogEntry:
"""Parsed log entry."""
timestamp: datetime
level: str
message: str

def __str__(self) -> str:
return f"[{self.level}] {self.timestamp}: {self.message}"


class LogParser:
"""Parse and process log files."""

# Log line pattern: [2024-01-15 10:30:45] ERROR Some message
PATTERN = re.compile(
r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+) (.*)'
)

@staticmethod
def parse_line(line: str) -> LogEntry:
"""Parse single log line."""
match = LogParser.PATTERN.match(line.strip())

if not match:
raise InvalidLogFormat(f"Invalid log format: {line}")

timestamp_str, level, message = match.groups()
timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")

return LogEntry(timestamp, level, message)

@staticmethod
def read_logs(filepath: str) -> Generator[LogEntry, None, None]:
"""
Generator that yields parsed log entries.
Memory efficient for large files.
"""
try:
with open(filepath) as f:
for line_num, line in enumerate(f, 1):
if not line.strip(): # Skip empty lines
continue

try:
yield LogParser.parse_line(line)
except InvalidLogFormat as e:
print(f"Line {line_num}: {e}")
continue

except FileNotFoundError as e:
raise LogFileError(f"Log file not found: {filepath}") from e

@staticmethod
def filter_by_level(logs: Generator[LogEntry, None, None],
level: str) -> Generator[LogEntry, None, None]:
"""Generator that filters logs by level."""
for log in logs:
if log.level == level:
yield log

@staticmethod
def count_by_level(filepath: str) -> Dict[str, int]:
"""Count logs by level (uses generator internally)."""
counts: Dict[str, int] = {}

for log in LogParser.read_logs(filepath):
counts[log.level] = counts.get(log.level, 0) + 1

return counts

@staticmethod
def get_errors_after(filepath: str, timestamp: datetime) -> Generator[LogEntry, None, None]:
"""Get error logs after specific timestamp."""
for log in LogParser.read_logs(filepath):
if log.level == "ERROR" and log.timestamp >= timestamp:
yield log


class LogManager:
"""Context manager for log file operations."""

def __init__(self, filepath: str):
self.filepath = filepath
self.logs: list[LogEntry] = []

def __enter__(self) -> 'LogManager':
"""Load logs on entering context."""
try:
for log in LogParser.read_logs(self.filepath):
self.logs.append(log)
except LogFileError as e:
print(f"Error loading logs: {e}")

return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Cleanup on exiting context."""
self.logs.clear()

def get_summary(self) -> Dict[str, int]:
"""Get log summary."""
return LogParser.count_by_level(self.filepath)

def find_errors(self) -> list[LogEntry]:
"""Find all error logs."""
return [log for log in self.logs if log.level == "ERROR"]


# Usage
if __name__ == "__main__":
# Using context manager
print("=== Using Context Manager ===")
with LogManager("logs/app.log") as manager:
summary = manager.get_summary()
print("Log summary:", summary)

errors = manager.find_errors()
print(f"Found {len(errors)} errors")

# Using generators directly
print("\n=== Using Generators ===")
error_count = 0
for error_log in LogParser.filter_by_level(
LogParser.read_logs("logs/app.log"),
"ERROR"
):
print(f"Error: {error_log.message}")
error_count += 1

print(f"Total errors: {error_count}")

# Memory efficient - doesn't load entire file
print(f"\nMemory usage: Generators process line-by-line")

Try itโ€‹

Generate a 100,000-line sample log with a short script, run the parser on it, and add a test that malformed lines are skipped rather than crashing the parser.


Milestone 7: Real-World Skillsโ€‹

In short: build a small pipeline that reads CSV/JSON files, cleans the data, and times itself.

Mini-Project: Data Processing Pipelineโ€‹

Goal: Practice file I/O, regex, and profiling

Implementationโ€‹

"""Data processing pipeline for CSV analysis."""
import csv
import re
from pathlib import Path
from typing import List, Dict
import time
from functools import wraps


def timing_decorator(func):
"""Measure function execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
print(f"{func.__name__} took {duration:.4f}s")
return result
return wrapper


class DataProcessor:
"""Process CSV data with validation."""

EMAIL_PATTERN = re.compile(r'^[\w\.-]+@[\w\.-]+\.\w+$')
PHONE_PATTERN = re.compile(r'^\+?\d{10,14}$')

@staticmethod
@timing_decorator
def load_csv(filepath: str) -> List[Dict]:
"""Load CSV file."""
data = []
with open(filepath) as f:
reader = csv.DictReader(f)
for row in reader:
data.append(row)
return data

@staticmethod
def validate_email(email: str) -> bool:
"""Validate email format."""
return bool(DataProcessor.EMAIL_PATTERN.match(email))

@staticmethod
def validate_phone(phone: str) -> bool:
"""Validate phone format."""
return bool(DataProcessor.PHONE_PATTERN.match(phone))

@staticmethod
@timing_decorator
def process_data(data: List[Dict]) -> Dict:
"""Process and validate data."""
results = {
'total': len(data),
'valid_emails': 0,
'valid_phones': 0,
'invalid_records': []
}

for i, record in enumerate(data):
email_valid = DataProcessor.validate_email(record.get('email', ''))
phone_valid = DataProcessor.validate_phone(record.get('phone', ''))

if email_valid:
results['valid_emails'] += 1
if phone_valid:
results['valid_phones'] += 1

if not (email_valid and phone_valid):
results['invalid_records'].append(i)

return results

@staticmethod
@timing_decorator
def save_results(results: Dict, filepath: str) -> None:
"""Save results to file."""
Path(filepath).write_text(str(results))


# Usage
if __name__ == "__main__":
# Create sample CSV
csv_data = """name,email,phone
Alice,alice@example.com,+12025551234
Bob,bob@invalid,5551234567
Charlie,charlie@example.com,12025551234
Diana,diana@example.com,invalid"""

Path("data.csv").write_text(csv_data)

# Process
data = DataProcessor.load_csv("data.csv")
results = DataProcessor.process_data(data)
DataProcessor.save_results(results, "results.txt")

print(f"\nResults: {results}")

Try itโ€‹

Time the pipeline on a small and a 10ร— larger input. Does the time grow 10ร—? Write down what you found in your project's README.


After Milestone 7: your portfolioโ€‹

By completing Milestone 7, you've built 7 mini-projects demonstrating:

MilestoneProjectKey Skills
1Config ManagerType hints, virtualenv setup, classes
2API Response ParserCollections, comprehensions, JSON
3Email ValidatorConditionals, guard clauses, regex
4Rate LimiterDecorators, closures, functional patterns
5User ManagementOOP, inheritance, special methods
6Log ParserGenerators, context managers, exceptions
7Data PipelineFile I/O, regex, profiling

If you did the Try it tasks, you'll also have tests for every project and a few timing measurements.

Portfolio Checkpointโ€‹

At this point, you have:

  • โœ… 7 working mini-projects
  • โœ… Professional code organization
  • โœ… Comprehensive test coverage
  • โœ… Performance profiling examples
  • โœ… Git history with meaningful commits

This is a solid foundation portfolio to show employers or build upon.


Milestone 8 onwards: role projectsโ€‹

In short: each role track ends in one larger project. The role guides contain worked code and a list of practice projects: SDET, SDE, SRE.

SDET: Test Automation Frameworkโ€‹

  • Milestone 8: Pytest framework with 50+ tests
  • Milestone 9: Mocking library for test isolation
  • Milestone 10: CI/CD pipeline setup
  • Milestones 11โ€“12: Selenium test suite for real application
  • Milestone 13: Capstone - Full QA automation suite

SDE: Backend Service Developmentโ€‹

  • Milestone 8: Package publishing to PyPI
  • Milestone 9: Async task queue (Celery)
  • Milestone 10: Database ORM integration
  • Milestone 11: REST API with error handling
  • Milestone 12: Security & authentication
  • Milestone 13: Capstone - Microservice with all patterns

SRE: Infrastructure & Monitoringโ€‹

  • Milestone 8: Infrastructure as code (Terraform + Python)
  • Milestone 9: Kubernetes automation
  • Milestone 10: Monitoring stack setup (Prometheus)
  • Milestone 11: Incident response automation
  • Milestone 12: Chaos engineering experiments
  • Milestones 13โ€“16: Capstone - Production-ready platform

Ways to work through this pageโ€‹

  1. Pick your role (SDET, SDE, or SRE)
  2. Start with Milestone 1, complete mini-project
  3. Move to Milestone 2, etc.
  4. After Milestone 7, follow your role track
  5. Keep GitHub repo of all projects
  6. At end, review entire portfolio

Option B: Self-Pacedโ€‹

  • Spend more time on challenging milestones
  • Extend mini-projects with additional features
  • Research topics deeply
  • Build larger capstone projects earlier

Option C: Team Learningโ€‹

  • Share mini-project implementations
  • Code review each other's projects
  • Discuss design decisions
  • Learn from different approaches

Tracking your progressโ€‹

Copy this template into a PROGRESS.md file in each project folder:

Milestone Checklist Templateโ€‹

# Milestone [N] Progress

## Learning Objectives
- [ ] Objective 1
- [ ] Objective 2
- [ ] Objective 3

## Mini-Project
- [ ] Setup complete
- [ ] Core functionality implemented
- [ ] Tests passing
- [ ] Code reviewed
- [ ] Committed to git

## Challenges Encountered
- Challenge 1: Solution
- Challenge 2: Solution

## Key Insights
- Insight 1
- Insight 2


Next Steps:

  1. Create a new directory for Milestone 1 project
  2. Follow the setup steps
  3. Implement the mini-project
  4. Write and run tests
  5. Commit to git
  6. Move to Milestone 2

Start with Milestone 1. You've got this!