Toolmingo
Guides26 min read

Python Roadmap 2025 (Complete Beginner to Advanced Guide)

The definitive Python roadmap for 2025 — from variables and data types to OOP, async, web APIs, data science, and landing your first Python developer job. Step-by-step with realistic timelines.

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:

  • threading does not achieve true CPU parallelism for Python code
  • threading does work for I/O-bound tasks because the GIL is released during I/O waits
  • multiprocessing bypasses 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).

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools