Python is the most versatile language in the world right now — it powers Instagram's backend, NASA's science pipelines, OpenAI's models, and millions of automation scripts. It has the gentlest learning curve of any general-purpose language and the broadest career options: web dev, data science, machine learning, DevOps, automation, and more. This roadmap shows you exactly what to learn, in what order, and how long each phase realistically takes in 2025.
At a glance
| Phase | Topics | Time estimate |
|---|---|---|
| 1 | Setup & Python basics | 2–3 weeks |
| 2 | Control flow & functions | 3–4 weeks |
| 3 | Data structures | 2–3 weeks |
| 4 | Object-oriented programming | 3–4 weeks |
| 5 | File I/O & modules | 2 weeks |
| 6 | Error handling & testing | 2 weeks |
| 7 | Concurrency | 3 weeks |
| 8 | Choose your path | 1 week |
| 9 | Web development path (FastAPI/Django) | 6–8 weeks |
| 10 | Production & deployment | 3–4 weeks |
| Total | From zero to job-ready | ~12–14 months |
Phase 1 — Setup & Python basics (2–3 weeks)
Installing Python
Always install from python.org or use pyenv to manage multiple versions. Never rely on the system Python on macOS/Linux.
# Check version (you want 3.11+ for 2025)
python --version
# Install pyenv (recommended)
curl https://pyenv.run | bash
pyenv install 3.12.3
pyenv global 3.12.3
Editor: VS Code with the Python extension (Pylance, debugger, test runner) is the standard choice. PyCharm Community is a strong alternative for those who prefer a full IDE.
Variables and types
Python is dynamically typed — you don't declare types, but they exist at runtime.
# Variables — no declaration keyword needed
name = "Alice"
age = 30
height = 1.75
is_active = True
nothing = None
# Check the type at runtime
print(type(name)) # <class 'str'>
print(type(age)) # <class 'int'>
print(type(height)) # <class 'float'>
| Type | Example | Notes |
|---|---|---|
int |
42, -7 |
Arbitrary precision — no overflow |
float |
3.14, -0.5 |
IEEE 754 double |
str |
"hello", 'world' |
Immutable, Unicode by default |
bool |
True, False |
Subclass of int (True == 1) |
NoneType |
None |
Python's null — use is None to check |
complex |
3+4j |
Built-in complex numbers |
Strings
# f-strings (prefer these over .format() and %)
name = "Alice"
age = 30
greeting = f"Hello, {name}! You are {age} years old."
# Common string methods
s = " Hello, World! "
print(s.strip()) # "Hello, World!"
print(s.lower()) # " hello, world! "
print(s.replace("o", "0"))
print(s.split(",")) # [' Hello', ' World! ']
print(",".join(["a", "b", "c"])) # "a,b,c"
# Multiline strings
sql = """
SELECT *
FROM users
WHERE active = true
"""
Operators
| Category | Operators | Notes |
|---|---|---|
| Arithmetic | + - * / // % ** |
// is floor division; ** is power |
| Comparison | == != < > <= >= |
Returns bool |
| Logical | and or not |
Short-circuit evaluation |
| Identity | is is not |
Checks object identity, not equality |
| Membership | in not in |
Works on any iterable |
| Walrus | := |
Assignment expression (Python 3.8+) |
Input / Output
# Output
print("Hello") # newline by default
print("a", "b", sep="-") # "a-b"
print("loading", end="...") # no newline
# Input — always returns str
name = input("What is your name? ")
age = int(input("How old are you? ")) # cast explicitly
Phase 2 — Control flow & functions (3–4 weeks)
if / elif / else
score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"
# Ternary (conditional expression)
status = "pass" if score >= 60 else "fail"
Loops
# for loop — iterate over any iterable
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# enumerate — get index and value
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
# range
for i in range(0, 10, 2): # start, stop, step
print(i) # 0, 2, 4, 6, 8
# while loop
count = 0
while count < 5:
print(count)
count += 1
# Loop control
for n in range(10):
if n == 3:
continue # skip to next iteration
if n == 7:
break # exit loop
else:
print("loop finished without break") # runs if no break
Functions
# Basic function
def greet(name: str) -> str:
return f"Hello, {name}!"
# Default parameters
def power(base: float, exponent: float = 2) -> float:
return base ** exponent
# *args — variable positional arguments
def sum_all(*args: float) -> float:
return sum(args)
# **kwargs — variable keyword arguments
def make_profile(**kwargs):
return kwargs
# Combined
def full_function(required, *args, keyword_only=None, **kwargs):
pass
# Lambda — small anonymous functions
double = lambda x: x * 2
sorted_pairs = sorted([(1, "b"), (2, "a")], key=lambda pair: pair[1])
Scope (LEGB)
| Scope | Description | Example |
|---|---|---|
| Local | Inside current function | Variables defined in a def |
| Enclosing | Outer function in nested functions | Closure variables |
| Global | Module-level | Top-level variables |
| Built-in | Python's built-ins | len, range, print |
x = "global"
def outer():
x = "enclosing"
def inner():
nonlocal x # modify enclosing scope
x = "inner"
inner()
print(x) # "inner"
outer()
print(x) # "global" — unchanged
Phase 3 — Data structures (2–3 weeks)
Python's built-in data structures are powerful and should be your first choice before reaching for a library.
List
Ordered, mutable, allows duplicates.
nums = [3, 1, 4, 1, 5, 9]
nums.append(2) # add to end
nums.insert(0, 0) # insert at index
nums.remove(1) # remove first occurrence of value
popped = nums.pop() # remove and return last
nums.sort() # in-place sort
sorted_copy = sorted(nums) # returns new list
# Slicing
nums[1:4] # elements 1, 2, 3
nums[::-1] # reversed copy
nums[::2] # every other element
Tuple
Ordered, immutable, allows duplicates. Use for heterogeneous data, coordinates, or function returns.
point = (10, 20)
x, y = point # unpacking
r, *rest = (1, 2, 3, 4) # extended unpacking
# Named tuple — lightweight class-like struct
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(10, 20)
print(p.x, p.y)
Set
Unordered, mutable, no duplicates. O(1) membership test.
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a | b # union
a & b # intersection → {3, 4}
a - b # difference → {1, 2}
a ^ b # symmetric difference → {1, 2, 5, 6}
# Remove duplicates from a list
unique = list(set([1, 2, 2, 3, 3, 3]))
Dictionary
Key-value store, ordered (Python 3.7+), O(1) average lookup.
user = {"name": "Alice", "age": 30, "active": True}
user["email"] = "alice@example.com" # add/update
user.get("phone", "N/A") # safe get with default
del user["active"]
# Iterate
for key, value in user.items():
print(f"{key}: {value}")
# Merge (Python 3.9+)
defaults = {"theme": "dark", "lang": "en"}
settings = defaults | {"lang": "fr"} # {"theme": "dark", "lang": "fr"}
Comprehensions
# List comprehension
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
# Dict comprehension
word_lengths = {word: len(word) for word in ["apple", "banana", "cherry"]}
# Set comprehension
unique_lengths = {len(word) for word in ["cat", "dog", "elephant"]}
# Generator expression — lazy, memory-efficient
total = sum(x**2 for x in range(1_000_000))
Choosing the right structure
| Need | Use |
|---|---|
| Ordered mutable sequence | list |
| Immutable record / return value | tuple |
| Unique items / fast membership test | set |
| Key-value mapping | dict |
| Queue (FIFO) | collections.deque |
| Counter | collections.Counter |
| Default values | collections.defaultdict |
| Ordered dict (insertion order matters) | dict (3.7+) |
Useful built-in functions
numbers = [3, 1, 4, 1, 5, 9]
len(numbers) # 6
sum(numbers) # 23
min(numbers) # 1
max(numbers) # 9
sorted(numbers) # [1, 1, 3, 4, 5, 9]
reversed(numbers) # iterator
enumerate(numbers) # (0, 3), (1, 1), ...
zip([1,2,3], ["a","b","c"]) # (1,"a"), (2,"b"), (3,"c")
map(str, numbers) # iterator of strings
filter(lambda x: x > 3, numbers) # iterator of 4, 5, 9
any(x > 8 for x in numbers) # True
all(x > 0 for x in numbers) # True
Phase 4 — Object-oriented programming (3–4 weeks)
Classes and instances
class Dog:
# Class attribute — shared by all instances
species = "Canis lupus familiaris"
def __init__(self, name: str, age: int) -> None:
# Instance attributes
self.name = name
self.age = age
def bark(self) -> str:
return f"{self.name} says: Woof!"
def __repr__(self) -> str:
return f"Dog(name={self.name!r}, age={self.age})"
def __str__(self) -> str:
return f"{self.name} ({self.age} years old)"
rex = Dog("Rex", 3)
print(rex.bark()) # Rex says: Woof!
print(repr(rex)) # Dog(name='Rex', age=3)
Inheritance
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
raise NotImplementedError
class Cat(Animal):
def speak(self) -> str:
return f"{self.name} says: Meow!"
class Dog(Animal):
def speak(self) -> str:
return f"{self.name} says: Woof!"
# Multiple inheritance
class ServiceDog(Dog):
def __init__(self, name: str, role: str) -> None:
super().__init__(name)
self.role = role
Dunder (magic) methods
| Method | When called |
|---|---|
__init__ |
Object creation |
__repr__ |
repr(obj) — developer-facing string |
__str__ |
str(obj) — user-facing string |
__len__ |
len(obj) |
__eq__ |
obj == other |
__lt__ |
obj < other (enables sorting) |
__add__ |
obj + other |
__contains__ |
item in obj |
__iter__ |
for item in obj |
__enter__ / __exit__ |
with obj: context manager |
__getitem__ |
obj[key] |
Properties
class Circle:
def __init__(self, radius: float) -> None:
self._radius = radius
@property
def radius(self) -> float:
return self._radius
@radius.setter
def radius(self, value: float) -> None:
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self) -> float:
import math
return math.pi * self._radius ** 2
Dataclasses
For simple data-holding classes, dataclass eliminates boilerplate:
from dataclasses import dataclass, field
from typing import List
@dataclass
class User:
name: str
email: str
age: int = 0
tags: List[str] = field(default_factory=list)
def __post_init__(self):
self.email = self.email.lower()
user = User("Alice", "Alice@Example.com", 30)
print(user) # User(name='Alice', email='alice@example.com', age=30, tags=[])
Abstract Base Classes
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
class Rectangle(Shape):
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
# Shape() # TypeError: Can't instantiate abstract class
rect = Rectangle(4, 5)
print(rect.area()) # 20.0
Phase 5 — File I/O & modules (2 weeks)
Reading and writing files
# Always use context managers — they close the file automatically
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read() # entire file as string
# or
lines = f.readlines() # list of lines
# or
for line in f: # memory-efficient iteration
print(line.strip())
# Writing
with open("output.txt", "w", encoding="utf-8") as f:
f.write("Hello, World!\n")
# Appending
with open("log.txt", "a", encoding="utf-8") as f:
f.write("New log entry\n")
pathlib — modern path handling
from pathlib import Path
base = Path("data")
file = base / "users.csv" # path joining with /
print(file.exists())
print(file.suffix) # ".csv"
print(file.stem) # "users"
print(file.parent) # PosixPath("data")
# Iterate directory
for path in Path(".").glob("**/*.py"):
print(path)
# Read / write with pathlib
text = Path("readme.md").read_text(encoding="utf-8")
Path("output.txt").write_text("content", encoding="utf-8")
JSON and CSV
import json
import csv
# JSON
data = {"name": "Alice", "scores": [95, 87, 92]}
json_str = json.dumps(data, indent=2)
parsed = json.loads(json_str)
with open("data.json", "w") as f:
json.dump(data, f, indent=2)
with open("data.json") as f:
loaded = json.load(f)
# CSV
import csv
with open("users.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "age"])
writer.writeheader()
writer.writerow({"name": "Alice", "age": 30})
with open("users.csv", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
print(row)
Virtual environments and pip
# Create a virtual environment
python -m venv .venv
# Activate (bash/zsh)
source .venv/bin/activate
# Activate (Windows cmd)
.venv\Scripts\activate.bat
# Install packages
pip install fastapi httpx pytest
# Save dependencies
pip freeze > requirements.txt
# Install from requirements
pip install -r requirements.txt
# Modern alternative: uv (10-100x faster than pip)
pip install uv
uv venv && uv pip install fastapi
Module system
# my_module.py
def helper():
return "I am a helper"
# main.py
import my_module
from my_module import helper
from my_module import helper as h
# Packages — directory with __init__.py
# mypackage/
# __init__.py
# utils.py
# models.py
from mypackage.utils import something
| stdlib module | Use for |
|---|---|
os |
OS operations, environment variables |
sys |
Python runtime info, argv |
pathlib |
Modern file path handling |
json |
JSON encode/decode |
csv |
CSV read/write |
datetime |
Dates and times |
re |
Regular expressions |
logging |
Structured logging |
argparse |
CLI argument parsing |
collections |
Counter, deque, defaultdict |
itertools |
Infinite iterators, combinations |
functools |
lru_cache, partial, reduce |
typing |
Type hints |
dataclasses |
@dataclass decorator |
abc |
Abstract base classes |
unittest |
Testing framework |
threading |
Thread-based concurrency |
multiprocessing |
Process-based parallelism |
asyncio |
Async/await event loop |
Phase 6 — Error handling & testing (2 weeks)
Exception handling
# Basic try/except
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}")
# Multiple exceptions
try:
value = int(input("Enter number: "))
data = {"key": "value"}
print(data[value])
except ValueError:
print("Not a valid integer")
except KeyError as e:
print(f"Key not found: {e}")
except (TypeError, AttributeError) as e:
print(f"Type error: {e}")
else:
print("No exception occurred") # runs if no exception
finally:
print("Always runs") # cleanup here
Custom exceptions
class AppError(Exception):
"""Base exception for this application."""
class ValidationError(AppError):
def __init__(self, field: str, message: str) -> None:
self.field = field
self.message = message
super().__init__(f"Validation error on '{field}': {message}")
class NotFoundError(AppError):
def __init__(self, resource: str, id: int) -> None:
super().__init__(f"{resource} with id {id} not found")
# Raise with context
try:
raise ValidationError("email", "invalid format")
except ValidationError as e:
print(e.field, e.message)
Exception hierarchy
BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
├── ValueError
├── TypeError
├── AttributeError
├── KeyError
├── IndexError
├── FileNotFoundError (→ OSError)
├── PermissionError (→ OSError)
├── ZeroDivisionError (→ ArithmeticError)
├── OverflowError (→ ArithmeticError)
├── StopIteration
└── RuntimeError
Testing with pytest
pip install pytest pytest-cov
# test_math_utils.py
import pytest
from math_utils import add, divide
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, -1) == -2
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
pytest # run all tests
pytest -v # verbose output
pytest --cov=. --cov-report=html # with coverage report
pytest -k "test_add" # run matching tests
pytest -x # stop on first failure
Mocking
from unittest.mock import patch, MagicMock
import pytest
def test_send_email_calls_smtp(mock_smtp):
with patch("mymodule.smtplib.SMTP") as mock_smtp:
mock_instance = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_instance
from mymodule import send_email
send_email("test@example.com", "Hello")
mock_instance.sendmail.assert_called_once()
Phase 7 — Concurrency (3 weeks)
Threading vs multiprocessing vs asyncio
| Feature | threading |
multiprocessing |
asyncio |
|---|---|---|---|
| Best for | I/O-bound tasks | CPU-bound tasks | I/O-bound, high concurrency |
| Parallelism | No (GIL) | Yes (separate processes) | No (single thread) |
| Memory | Shared | Separate (copy-on-write) | Shared |
| Overhead | Low | High (process spawn) | Very low |
| Communication | Shared objects + locks | Queue, Pipe | await, Queue |
| Use case | File I/O, HTTP requests | Image processing, ML | Web servers, APIs |
The GIL explained
The Global Interpreter Lock (GIL) is a mutex in CPython that allows only one thread to execute Python bytecode at a time. This means:
threadingdoes not achieve true CPU parallelism for Python codethreadingdoes work for I/O-bound tasks because the GIL is released during I/O waitsmultiprocessingbypasses the GIL by using separate processes, each with its own GIL- Python 3.13 introduced an experimental free-threaded mode (
--disable-gil) — watch this space
asyncio — the modern approach for I/O
import asyncio
import httpx
async def fetch_url(client: httpx.AsyncClient, url: str) -> dict:
response = await client.get(url)
response.raise_for_status()
return response.json()
async def fetch_all(urls: list[str]) -> list[dict]:
async with httpx.AsyncClient() as client:
tasks = [fetch_url(client, url) for url in urls]
results = await asyncio.gather(*tasks)
return results
async def main():
urls = [
"https://api.example.com/users/1",
"https://api.example.com/users/2",
"https://api.example.com/users/3",
]
data = await fetch_all(urls)
for item in data:
print(item)
if __name__ == "__main__":
asyncio.run(main())
threading for I/O-bound work
import threading
import queue
import time
def worker(task_queue: queue.Queue, results: list) -> None:
while True:
task = task_queue.get()
if task is None:
break
time.sleep(0.1) # simulate I/O
results.append(task * 2)
task_queue.task_done()
task_queue = queue.Queue()
results = []
threads = [threading.Thread(target=worker, args=(task_queue, results)) for _ in range(4)]
for t in threads:
t.start()
for i in range(20):
task_queue.put(i)
task_queue.join()
for _ in threads:
task_queue.put(None)
for t in threads:
t.join()
multiprocessing for CPU-bound work
from multiprocessing import Pool
import math
def is_prime(n: int) -> bool:
if n < 2:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
if __name__ == "__main__":
numbers = range(10_000, 100_000)
with Pool() as pool:
primes = [n for n, result in zip(numbers, pool.map(is_prime, numbers)) if result]
print(f"Found {len(primes)} primes")
Phase 8 — Choose your path (1 week)
Python's breadth is its greatest strength. After the core language, pick one path and go deep before branching out.
| Path | Main frameworks / tools | Salary range (US) | Job market |
|---|---|---|---|
| Web Development | FastAPI, Django, Flask | $90k–$160k | Strong |
| Data Science | Pandas, NumPy, Jupyter, Matplotlib | $100k–$170k | Very strong |
| Machine Learning / AI | PyTorch, TensorFlow, scikit-learn, HuggingFace | $130k–$250k | Explosive |
| Automation / Scripting | Selenium, Playwright, Scrapy, Airflow | $80k–$140k | Stable |
| DevOps / Infrastructure | Ansible, Terraform (Python SDK), AWS Boto3 | $110k–$180k | Strong |
| CLI tools | Click, Typer, Rich | $90k–$150k | Niche but valued |
| Embedded / IoT | MicroPython, CircuitPython | $85k–$145k | Growing |
This roadmap continues with the Web Development path as it covers the most broadly applicable backend engineering concepts.
Phase 9 — Web development path: FastAPI (6–8 weeks)
FastAPI basics
FastAPI is the modern standard for Python APIs. It is built on Starlette (ASGI), uses Pydantic for validation, and auto-generates OpenAPI docs.
pip install fastapi uvicorn[standard] httpx
uvicorn main:app --reload
# main.py
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, EmailStr
from typing import Optional
app = FastAPI(title="My API", version="1.0.0")
class UserCreate(BaseModel):
name: str
email: EmailStr
age: int = 0
class UserResponse(BaseModel):
id: int
name: str
email: str
class Config:
from_attributes = True
# In-memory store for demonstration
users_db: dict[int, dict] = {}
next_id = 1
@app.get("/health")
async def health() -> dict:
return {"status": "ok"}
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate) -> UserResponse:
global next_id
new_user = {"id": next_id, **user.model_dump()}
users_db[next_id] = new_user
next_id += 1
return UserResponse(**new_user)
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int) -> UserResponse:
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
return UserResponse(**users_db[user_id])
Pydantic models
Pydantic v2 is the validation backbone of FastAPI. Know it well.
from pydantic import BaseModel, Field, field_validator, model_validator
from datetime import datetime
from typing import Optional
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="Price in USD")
stock: int = Field(ge=0, default=0)
created_at: datetime = Field(default_factory=datetime.utcnow)
tags: list[str] = []
@field_validator("name")
@classmethod
def name_must_not_be_blank(cls, v: str) -> str:
if not v.strip():
raise ValueError("name cannot be blank")
return v.strip()
@model_validator(mode="after")
def check_stock_with_price(self) -> "Product":
if self.price > 1000 and self.stock == 0:
raise ValueError("Expensive items must have stock > 0")
return self
SQLAlchemy ORM
pip install sqlalchemy alembic psycopg2-binary
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from datetime import datetime
DATABASE_URL = "postgresql://user:pass@localhost/mydb"
engine = create_engine(DATABASE_URL, echo=False)
SessionLocal = sessionmaker(bind=engine)
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
email = Column(String(255), unique=True, nullable=False, index=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Dependency injection for FastAPI
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Repository pattern
class UserRepository:
def __init__(self, db: Session) -> None:
self.db = db
def create(self, name: str, email: str) -> User:
user = User(name=name, email=email)
self.db.add(user)
self.db.commit()
self.db.refresh(user)
return user
def find_by_id(self, user_id: int) -> User | None:
return self.db.query(User).filter(User.id == user_id).first()
def find_all(self, skip: int = 0, limit: int = 100) -> list[User]:
return self.db.query(User).offset(skip).limit(limit).all()
Authentication with JWT
pip install python-jose[cryptography] passlib[bcrypt]
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
SECRET_KEY = "your-secret-key-from-env"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
return username
Django — the full-stack alternative
If you prefer batteries-included, use Django + Django REST Framework.
| Feature | FastAPI | Django + DRF |
|---|---|---|
| Philosophy | Minimal, build your own | Batteries included |
| ORM | SQLAlchemy (external) | Django ORM (built-in) |
| Admin panel | None (use custom) | Built-in, powerful |
| Auth | Manual or libraries | Built-in |
| Async | First-class (ASGI) | Partial (3.1+) |
| API docs | Auto OpenAPI | Manual or drf-spectacular |
| Learning curve | Low | Medium |
| Best for | Microservices, APIs | Monoliths, CMS, admin-heavy |
Phase 10 — Production & deployment (3–4 weeks)
Docker
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first (for better layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Non-root user for security
RUN adduser --disabled-password --gecos "" appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
version: "3.9"
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/mydb
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
CI/CD with GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Lint with ruff
run: ruff check .
- name: Type check with mypy
run: mypy .
- name: Run tests
run: pytest --cov=. --cov-report=xml
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
- name: Upload coverage
uses: codecov/codecov-action@v4
Logging
import logging
import sys
from typing import Any
def setup_logging(level: str = "INFO") -> None:
logging.basicConfig(
level=getattr(logging, level.upper()),
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)
# In your code
logger.debug("Cache miss for key %s", key)
logger.info("User %d logged in", user_id)
logger.warning("Rate limit approaching for IP %s", ip)
logger.error("Database connection failed", exc_info=True)
logger.critical("Out of memory, shutting down")
Environment management
# config.py — never hardcode secrets
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
secret_key: str
debug: bool = False
allowed_origins: list[str] = ["http://localhost:3000"]
log_level: str = "INFO"
class Config:
env_file = ".env"
case_sensitive = False
settings = Settings()
Deployment options
| Platform | Best for | Cost | Complexity |
|---|---|---|---|
| Railway | Small projects, side projects | Low | Very low |
| Render | Web services, hobby projects | Free tier | Low |
| Fly.io | Global edge deployment | Low | Medium |
| AWS ECS / Fargate | Production, scale | Medium–High | High |
| Google Cloud Run | Serverless containers | Pay-per-use | Medium |
| DigitalOcean App Platform | Straightforward deploys | Low–Medium | Low |
| Kubernetes (EKS/GKE) | Large-scale microservices | High | Very high |
Full technology stack map
PYTHON DEVELOPER STACK
═══════════════════════════════════════════════════════════════════════
LANGUAGE CORE
├── Python 3.12+
├── Type hints (typing, generic types)
├── Async/await (asyncio)
└── Standard library (pathlib, json, logging, re, datetime...)
PACKAGE MANAGEMENT
├── pip + venv (universal)
├── uv (modern, fast)
└── poetry (dependency locking)
CODE QUALITY
├── ruff (linting + formatting, replaces flake8 + black)
├── mypy (static type checking)
└── pre-commit (git hooks)
TESTING
├── pytest (test runner)
├── pytest-cov (coverage)
├── httpx (async HTTP testing)
└── factory-boy (test fixtures)
WEB FRAMEWORK (pick one)
├── FastAPI ──→ Pydantic v2, Starlette, uvicorn
└── Django ──→ Django ORM, DRF, Channels
DATABASE
├── PostgreSQL (primary choice)
├── SQLAlchemy (ORM for FastAPI)
├── Django ORM (if using Django)
├── Alembic (migrations for SQLAlchemy)
└── Redis (cache, queues)
AUTHENTICATION
├── python-jose (JWT)
├── passlib (password hashing)
└── django-allauth (if Django)
ASYNC TASKS
├── Celery + Redis
└── arq (async Redis Queue)
DATA SCIENCE / ML PATH
├── NumPy (numerical arrays)
├── Pandas (data manipulation)
├── Matplotlib / Seaborn (visualization)
├── scikit-learn (classical ML)
├── PyTorch (deep learning)
└── HuggingFace Transformers (LLMs)
INFRASTRUCTURE
├── Docker + docker-compose
├── GitHub Actions (CI/CD)
├── Nginx / Caddy (reverse proxy)
└── PostgreSQL + Redis (production data layer)
MONITORING
├── Sentry (error tracking)
├── Prometheus + Grafana (metrics)
└── structlog / loguru (structured logging)
Realistic 52-week timeline
| Weeks | Phase | Milestone |
|---|---|---|
| 1–3 | Setup & Python basics | Run your first script; understand types, strings, operators |
| 4–7 | Control flow & functions | Build a CLI calculator with error handling |
| 8–10 | Data structures | Write a contact book using dicts and lists |
| 11–14 | OOP | Build an OOP-based library management system |
| 15–16 | File I/O & modules | Parse JSON config files; build a CSV report generator |
| 17–18 | Error handling & testing | Achieve 80%+ test coverage on your library project |
| 19–21 | Concurrency | Build a concurrent URL checker with asyncio |
| 22 | Choose path | Research frameworks; set up dev environment for chosen path |
| 23–30 | Web development (FastAPI) | Build a full REST API with auth, DB, and OpenAPI docs |
| 31–34 | Database / ORM | Add PostgreSQL, migrations, and complex queries |
| 35–38 | Production | Dockerize, add CI/CD, deploy to Railway or Fly.io |
| 39–42 | Portfolio projects | Build 2–3 polished projects; write README, deploy |
| 43–46 | Job search prep | LeetCode (Python), system design, mock interviews |
| 47–52 | Apply & iterate | Apply to 5–10 positions per week; keep coding daily |
Portfolio projects
| Project | Description | Skills covered |
|---|---|---|
| URL shortener API | REST API with FastAPI, PostgreSQL, Redis caching, rate limiting | FastAPI, SQLAlchemy, Redis, Docker |
| Job board scraper | Scrape job listings with Playwright, store to DB, send digest email | Playwright, Celery, SMTP, scheduling |
| Personal finance tracker | CLI + web dashboard to categorize transactions from CSV exports | Pandas, Matplotlib, Click, SQLite |
| GitHub stats bot | Discord/Slack bot that posts daily GitHub activity summaries | GitHub API, asyncio, discord.py/slack-sdk |
| ML image classifier | Train a CNN on custom dataset, expose as FastAPI endpoint | PyTorch, Pillow, FastAPI, Docker |
| Distributed task queue | Custom mini-Celery using Redis pub/sub and multiprocessing | Redis, multiprocessing, asyncio, logging |
Python developer roles & salaries
| Role | Experience | Tech stack | US salary range |
|---|---|---|---|
| Junior Python Developer | 0–2 years | Python basics, one framework, Git | $65k–$95k |
| Mid-level Python Developer | 2–5 years | FastAPI or Django, PostgreSQL, Docker, CI/CD | $95k–$135k |
| Senior Python Developer | 5+ years | System design, distributed systems, mentoring | $135k–$180k |
| Data Scientist | 2–5 years | Pandas, NumPy, scikit-learn, SQL, Jupyter | $110k–$160k |
| ML Engineer | 3–6 years | PyTorch/TensorFlow, MLOps, model deployment | $140k–$220k |
| Python DevOps / Platform Engineer | 3–6 years | Boto3, Ansible, Terraform, Kubernetes | $130k–$190k |
Salaries vary significantly by location, company size, and sector. Remote roles and FAANG/AI companies pay 20–50% above median.
Common mistakes
| Mistake | Why it's bad | Fix |
|---|---|---|
Using mutable default arguments (def f(lst=[])) |
The list is created once and shared across all calls — silent bugs | Use None as default, create inside function |
Bare except: without specifying exception |
Catches SystemExit and KeyboardInterrupt; hides real errors |
Always specify except SomeError |
Comparing to None with == |
Works but semantically wrong; some objects override __eq__ |
Use is None or is not None |
| Opening files without a context manager | File may not be closed on exception, causing resource leaks | Always use with open(...) as f: |
Using time.sleep() in async code |
Blocks the event loop — all other coroutines freeze | Use await asyncio.sleep() |
| Not using virtual environments | Package conflicts between projects; pollutes system Python | Always python -m venv .venv |
Concatenating strings in a loop (s += ...) |
O(n²) complexity because strings are immutable (each concat copies) | Use "".join(list) or io.StringIO |
import * from modules |
Pollutes namespace; impossible to trace where names come from | Always use explicit imports |
Python vs other languages
| Dimension | Python | JavaScript | Java | Go | Ruby |
|---|---|---|---|---|---|
| Learning curve | Very low | Low–Medium | High | Medium | Low |
| Performance | Slow (CPython) | Fast (V8 JIT) | Fast (JVM JIT) | Very fast (compiled) | Slow |
| Concurrency | asyncio / threads (GIL) | Event loop (Node) | Threads + virtual threads | Goroutines (excellent) | Threads |
| Type system | Dynamic + optional hints | Dynamic + TypeScript | Static, strong | Static, strong | Dynamic |
| Web frameworks | FastAPI, Django | Express, Next.js | Spring | Gin, Echo | Rails |
| ML / AI | Dominant (PyTorch, TF) | Growing (TF.js) | Limited | Limited | Minimal |
| DevOps / scripts | Excellent | Good (Node) | Poor | Very good | Good |
| Job market | Very strong, broad | Very strong | Strong (enterprise) | Growing (infra) | Niche (Rails) |
| Community size | #1 language (TIOBE) | #1 by usage | Very large | Growing fast | Medium |
FAQ
Which Python version should I use in 2025? Python 3.12 or 3.13. Python 3.11 introduced massive performance improvements (10–60% faster than 3.10), and 3.12/3.13 continue those gains. Never use Python 2 — it reached end-of-life in 2020. Check your cloud platform and library compatibility before upgrading to the very latest.
What are the best resources for learning Python?
- Automate the Boring Stuff with Python by Al Sweigart (free online) — best for absolute beginners
- Python Crash Course by Eric Matthes — good structured intro with projects
- Fluent Python by Luciano Ramalho — deep dive for intermediate/advanced
- Real Python (realpython.com) — tutorials covering every topic
- Official docs (docs.python.org) — always accurate, often underused
- Fast.ai — if your goal is ML/AI
How long does it take to get a Python developer job? With consistent daily practice (2–4 hours), most people land their first job in 12–18 months starting from zero. Backgrounds in other programming languages can cut that to 4–8 months. Bootcamp graduates typically take 6–12 months post-graduation. Speed depends heavily on portfolio quality and networking.
Should I learn Python for ML or web development first? Learn core Python first (phases 1–7 of this roadmap) regardless of path — that content is identical. Then choose. If you want the fastest path to employment, web development has more entry-level openings. If you are passionate about AI/ML, go that route — the market is exceptional but the barrier is higher (linear algebra, statistics, ML theory).
Should I add type hints to my Python code?
Yes. Always. Type hints do not affect runtime performance but dramatically improve code readability, IDE support (autocomplete, inline errors), and catch bugs before they hit production. Use mypy or pyright to enforce them. FastAPI requires Pydantic models (which use type hints) — you will not escape them anyway.
Does the GIL mean Python is bad for backend services? No. The vast majority of web server bottlenecks are I/O-bound (waiting for the database, external APIs, disk). asyncio handles I/O-bound concurrency perfectly without needing multiple threads. Instagram, Spotify, and Dropbox run Python backends at massive scale. If you have specific CPU-bound bottlenecks, you can offload them to separate processes, Celery workers, or write C extensions (NumPy does this).