Home · Python & FastAPI · C# · .NET · SQL · TypeScript & React · System Design · Interview Prep · Algo Patterns · SQL
⚡ Developer Reference

Python & FastAPI
Crash Course

A fast-track reference for experienced developers coming from C# and Node.js. Pattern-match, don't re-learn.

01

Python Fundamentals

Variables & Types

No type declarations needed — Python is dynamically typed. But type hints exist (and FastAPI relies on them heavily).

# Basic types
name: str = "Hari"
age: int = 30
price: float = 9.99
active: bool = True
nothing: None = None

# Type is inferred — hints are optional everywhere except FastAPI
count = 42  # still an intpython

Strings

Python's f-strings (prefix f"...") embed expressions directly into string literals — equivalent to C# string interpolation or JS template literals. Triple-quoted strings ("""...""") span multiple lines without escape sequences, making them ideal for SQL queries and multiline messages.

# f-strings (like JS template literals / C# string interpolation)
greeting = f"Hello, {name}. You are {age} years old."

# Multi-line
query = """
SELECT *
FROM users
WHERE active = true
"""

# Common methods
"hello world".upper()          # "HELLO WORLD"
"Hello World".lower()          # "hello world"
"  padded  ".strip()            # "padded"
"a,b,c".split(",")              # ["a", "b", "c"]
",".join(["a", "b", "c"])     # "a,b,c"
"hello".startswith("he")       # True
"hello".replace("l", "L")     # "heLLo"python

Collections

📋 List []

Like C# List<T> or JS Array

fruits = ["apple", "banana"]
fruits.append("cherry")
fruits.insert(1, "blueberry")
fruits.pop()       # remove last
len(fruits)        # length

📌 Tuple ()

Immutable list

point = (10, 20)
x, y = point  # unpacking

# Great for return values
def get_coords():
    return 51.4, -4.3

🗂️ Dict {}

Like C# Dictionary or JS object

user = {"name": "Hari", "role": "admin"}
user["name"]                # "Hari"
user.get("email", "N/A")   # safe access
user["email"] = "h@x.com"  # add/update
del user["email"]           # delete

🎯 Set {}

Unique values, fast lookups

tags = {"python", "fastapi"}
tags.add("docker")
tags.discard("fastapi")
"python" in tags  # True

Slicing

Python's slice syntax seq[start:stop:step] works on any sequence (lists, strings, tuples). The stop index is exclusive, negative indices count from the end, and omitting a bound defaults to the sequence's start or end. This is far more concise than equivalent loops in C# or JS.

nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

nums[2:5]      # [2, 3, 4]       — start:stop (exclusive)
nums[:3]       # [0, 1, 2]       — first 3
nums[-3:]      # [7, 8, 9]       — last 3
nums[::2]      # [0, 2, 4, 6, 8] — every 2nd
nums[::-1]     # [9, 8, ..., 0]  — reversedpython

Control Flow

Unlike C#/JS, Python uses indentation (not braces) to define code blocks — a wrong indent is a syntax error. The ternary expression reads like English: value_if_true if condition else value_if_false. enumerate() gives you both the index and value in a loop, removing the need for a manual counter variable.

# if / elif / else — no braces, indentation matters
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
else:
    grade = "C"

# Ternary
status = "active" if is_enabled else "disabled"

# For loops
for item in collection:
    print(item)

for i, item in enumerate(collection):   # index + value
    print(f"{i}: {item}")

for key, value in user.items():         # dict iteration
    print(f"{key} = {value}")

# Range
for i in range(5):          # 0, 1, 2, 3, 4
for i in range(2, 8):       # 2, 3, 4, 5, 6, 7
for i in range(0, 10, 2):   # 0, 2, 4, 6, 8python

List Comprehensions

Learn these — they're everywhere in Python. The Pythonic way to transform and filter lists in a single expression.

A list comprehension replaces a for loop + append() pattern with a concise one-liner. Adding an if clause filters elements before they are included. The same syntax works for dicts ({k: v for ...}) and sets ({expr for ...}), and using parentheses instead of brackets gives a lazy generator.

# Basic: [expression for item in iterable]
squares = [x**2 for x in range(10)]

# With filter: [expression for item in iterable if condition]
evens = [x for x in range(20) if x % 2 == 0]

# Nested — flatten a matrix
flat = [cell for row in matrix for cell in row]

# Dict & Set comprehensions
squares = {x: x**2 for x in range(5)}
unique  = {x.lower() for x in words}python

Functions

Python functions are defined with def and support default parameter values, variadic positional args (*args), and variadic keyword args (**kwargs). Callers can pass any argument by name (keyword), which improves readability but means argument order matters less than in C#. Lambdas are anonymous single-expression functions, equivalent to JS arrow functions.

# Basic
def greet(name: str) -> str:
    return f"Hello, {name}"

# Default args
def connect(host: str, port: int = 5432) -> None:
    ...

# *args and **kwargs (like ...rest in JS)
def log(*args, **kwargs):
    print(args)    # tuple of positional args
    print(kwargs)  # dict of keyword args

log("info", "started", level=1, tag="boot")

# Lambda (arrow functions)
double = lambda x: x * 2
sorted_users = sorted(users, key=lambda u: u["name"])python

Unpacking & Destructuring

Python can unpack any iterable into named variables in a single assignment. The starred variable (*rest) acts like a catch-all and is equivalent to JS's rest syntax in destructuring. Dict unpacking with ** merges dictionaries — later keys override earlier ones, making it a clean alternative to Object.assign() or the spread operator.

# Tuple/list unpacking
a, b, c = [1, 2, 3]
first, *rest = [1, 2, 3, 4, 5]   # first=1, rest=[2,3,4,5]

# Dict unpacking (like JS spread)
defaults  = {"theme": "dark", "lang": "en"}
overrides = {"lang": "fr", "debug": True}
config = {**defaults, **overrides}
# {"theme": "dark", "lang": "fr", "debug": True}python
02

Core Concepts

Classes

Python classes use __init__ as the constructor, and every instance method receives self as its explicit first argument — there is no implicit this. Class-level variables are shared across all instances (like C# static fields), while instance variables are set on self. Inheritance is declared by putting the parent class in parentheses after the class name.

class Device:
    device_count: int = 0  # class var (like static)

    def __init__(self, serial: str, name: str):
        self.serial = serial
        self.name = name
        self.status = "offline"
        Device.device_count += 1

    def activate(self) -> None:
        self.status = "online"

# Inheritance
class POSDevice(Device):
    def __init__(self, serial: str, name: str, restaurant_id: str):
        super().__init__(serial, name)
        self.restaurant_id = restaurant_idpython

Dataclasses

Like C# records or DTO classes — auto-generates __init__, __repr__, __eq__

from dataclasses import dataclass, field

@dataclass
class Restaurant:
    id: str
    name: str
    active: bool = True
    tags: list[str] = field(default_factory=list)

r = Restaurant(id="r1", name="Pizza Palace")
print(r)  # Restaurant(id='r1', name='Pizza Palace', active=True, tags=[])python

Exception Handling

Python's try/except maps directly to C#'s try/catch. You can catch multiple exception types in one clause using a tuple, re-raise with a bare raise, and use else for code that only runs when no exception occurred. Custom exceptions are plain classes that inherit from Exception — no special interface is required.

try:
    result = risky_operation()
except ValueError as e:
    print(f"Bad value: {e}")
except (KeyError, TypeError):
    print("Key or type error")
except Exception as e:
    print(f"Unexpected: {e}")
    raise  # re-raise
else:
    print("No error occurred")
finally:
    cleanup()

# Custom exceptions
class DeviceNotFoundError(Exception):
    def __init__(self, serial: str):
        self.serial = serial
        super().__init__(f"Device {serial} not found")python

Context Managers

Like C# using / IDisposable — auto-cleanup on exit

# File I/O — auto-closes on exit
with open("data.json", "r") as f:
    data = json.load(f)

# Custom context manager
from contextlib import contextmanager

@contextmanager
def db_transaction(conn):
    tx = conn.begin()
    try:
        yield tx
        tx.commit()
    except:
        tx.rollback()
        raisepython

Decorators

Like C# attributes, but they actually wrap the function

import functools, time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.3f}s")
        return result
    return wrapper

@timer
def slow_query():
    time.sleep(1)
    return "done"python

Generators

Lazy iterators — memory efficient for large datasets

# Generator function (yields values one at a time)
def read_large_file(path: str):
    with open(path) as f:
        for line in f:
            yield line.strip()

# Generator expression (like lazy list comprehension)
squares = (x**2 for x in range(1_000_000))  # no memory spikepython

Type Hints

Critical for FastAPI. Type hints drive automatic request validation, serialisation, and Swagger doc generation.

Type hints are annotations only — Python does not enforce them at runtime by default, but tools like Pydantic and FastAPI read them via inspect to do real validation. Use X | None (Python 3.10+) or Optional[X] for nullable types, and built-in generics like list[str] instead of importing List from typing.

from typing import Optional, Callable

# Optional (can be None)
def find_user(id: str) -> dict | None: ...

# Collections
def get_ids() -> list[str]: ...
def get_config() -> dict[str, int]: ...

# Union
def parse(value: str | int) -> float: ...

# Callable
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)python

Async / Await

Same pattern as JS/C# — FastAPI is async-first

import asyncio

async def fetch_data(url: str) -> dict:
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()

# Run multiple concurrently (like Promise.all)
async def fetch_all():
    results = await asyncio.gather(
        fetch_data("https://api.example.com/a"),
        fetch_data("https://api.example.com/b"),
    )
    return resultspython

Enums

Mixing str into the Enum base class means each member behaves as a real string — you can compare it directly with string literals and JSON serialisation works automatically. This is the preferred pattern for FastAPI route parameters and Pydantic fields that have a fixed set of allowed values.

from enum import Enum

class DeviceStatus(str, Enum):
    ONLINE      = "online"
    OFFLINE     = "offline"
    MAINTENANCE = "maintenance"

# str, Enum lets you use it as a string directly
status = DeviceStatus.ONLINE
print(status.value)          # "online"
print(status == "online")    # True (because of str mixin)python
03

Virtual Environments

# Create a virtual environment $ python -m venv .venv # Activate it $ source .venv/bin/activate # macOS/Linux $ .venv\Scripts\activate # Windows # Install packages $ pip install fastapi uvicorn[standard] pydantic # Freeze dependencies $ pip freeze > requirements.txt $ pip install -r requirements.txt
04

Pydantic

Think of Pydantic models as C# DTOs with built-in validation. They power every request and response in FastAPI.

Basic Model

A Pydantic model is a class that inherits from BaseModel. Fields are declared as type-annotated class attributes, and validation runs automatically when you instantiate the model. Use Field(...) to add constraints — the ... means the field is required (no default). The @field_validator decorator lets you run custom transformation or validation logic on individual fields.

from pydantic import BaseModel, Field, field_validator
from datetime import datetime

class DeviceCreate(BaseModel):
    serial: str = Field(..., min_length=5, max_length=50)
    name: str
    restaurant_id: str
    tags: list[str] = []

    @field_validator("serial")
    @classmethod
    def serial_must_be_uppercase(cls, v: str) -> str:
        return v.upper()

class DeviceResponse(BaseModel):
    id: int
    serial: str
    name: str
    status: DeviceStatus
    created_at: datetime

    model_config = {"from_attributes": True}  # ORM modepython

Nested Models

Pydantic models can be nested by using one model as the type hint for a field in another. FastAPI will accept the corresponding nested JSON object and validate each level separately. This replaces the manual recursive parsing you'd write in C# with a JsonElement or nested DeserializeObject call.

class Address(BaseModel):
    street: str
    city: str
    postcode: str

class RestaurantCreate(BaseModel):
    name: str
    address: Address
    cuisine_tags: list[str] = []

# Accepts nested JSON automatically:
# { "name": "Pizza Palace", "address": {"street": "...", ...} }python

Usage

Instantiating a model validates and coerces input immediately — you get a typed object, not a raw dict. model_dump() converts back to a plain dict for serialisation or database writes, while model_dump_json() returns a JSON string. Validation errors surface as ValidationError with a structured list of per-field failures, which FastAPI automatically converts to a 422 response.

device = DeviceCreate(serial="sn-123", name="Till 1", restaurant_id="r42")
print(device.serial)           # "SN-123" (validator ran)
print(device.model_dump())     # → dict
print(device.model_dump_json()) # → JSON string

# Validation error
try:
    bad = DeviceCreate(serial="ab", name="X", restaurant_id="r1")
except ValidationError as e:
    print(e.errors())  # detailed error listpython
05

FastAPI

Project Structure

This layout separates concerns the same way ASP.NET Core does: routers act as controllers, services hold business logic, models are ORM entities, and schemas are the Pydantic request/response DTOs. Keeping schemas and models in separate folders prevents accidental coupling between your API surface and your database schema.

project/
├── app/
│   ├── __init__.py
│   ├── main.py           # FastAPI app & startup
│   ├── config.py          # Settings
│   ├── database.py        # DB session
│   ├── models/            # SQLAlchemy / ORM models
│   ├── schemas/           # Pydantic request/response models
│   ├── routers/           # Route handlers (controllers)
│   ├── services/          # Business logic
│   └── middleware/
├── tests/
├── requirements.txt
└── .envstructure

Basic App

The FastAPI() constructor is where you set global metadata that populates the auto-generated Swagger UI at /docs. CORS middleware must be added before any routers are included. include_router() mounts a router under a prefix — equivalent to app.UseEndpoints + MapControllerRoute in ASP.NET Core or app.use() in Express.

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(
    title="GrubDirect API",
    version="1.0.0",
    docs_url="/docs",       # Swagger UI at /docs
    redoc_url="/redoc",     # ReDoc at /redoc
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourdomain.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(devices.router, prefix="/api/devices", tags=["Devices"])

@app.get("/health")
async def health_check():
    return {"status": "ok"}app/main.py
# Run it $ uvicorn app.main:app --reload --port 8000 # Swagger docs → http://localhost:8000/docs

Router (Controller)

An APIRouter groups related endpoints the same way an ASP.NET controller class does. FastAPI reads the type hints on each handler function to derive query parameter names, path parameter types, and the expected request body schema — no attributes or manual binding code is needed. The response_model argument tells FastAPI which Pydantic schema to use for serialisation and output validation.

from fastapi import APIRouter, HTTPException, Query, Path, status

router = APIRouter()

# GET all — with query params
@router.get("/", response_model=list[DeviceResponse])
async def list_devices(
    status: str | None = Query(None, description="Filter by status"),
    skip: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
):
    return await device_service.get_all(status=status, skip=skip, limit=limit)

# GET by ID
@router.get("/{device_id}", response_model=DeviceResponse)
async def get_device(device_id: int = Path(..., gt=0)):
    device = await device_service.get_by_id(device_id)
    if not device:
        raise HTTPException(status_code=404, detail="Not found")
    return device

# POST — body parsed via Pydantic automatically
@router.post("/", response_model=DeviceResponse, status_code=201)
async def create_device(payload: DeviceCreate):
    return await device_service.create(payload)

# DELETE
@router.delete("/{device_id}", status_code=204)
async def delete_device(device_id: int):
    if not await device_service.delete(device_id):
        raise HTTPException(status_code=404, detail="Not found")app/routers/devices.py

Dependency Injection

FastAPI's DI is elegant — functions injected via Depends()

from fastapi import Depends, Header, HTTPException

# DB session dependency
async def get_db():
    db = SessionLocal()
    try:
        yield db  # like a context manager
    finally:
        db.close()

# Auth dependency
async def get_current_user(authorization: str = Header(...)):
    token = authorization.replace("Bearer ", "")
    user = verify_token(token)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid")
    return user

# Role guard factory
def require_role(role: str):
    def checker(user=Depends(get_current_user)):
        if user.role != role:
            raise HTTPException(status_code=403)
        return user
    return checker

# Usage
@router.get("/admin/devices")
async def admin_list(
    db=Depends(get_db),
    user=Depends(require_role("admin")),
): ...python

Background Tasks

FastAPI's BackgroundTasks runs a function after the HTTP response has been sent, freeing the client immediately. It's suitable for lightweight fire-and-forget work (emails, audit logs, webhooks) but runs in the same process — for CPU-heavy or long-running jobs use Celery or an async task queue instead.

from fastapi import BackgroundTasks

@router.post("/{device_id}/reboot")
async def reboot_device(device_id: int, bg: BackgroundTasks):
    await device_service.reboot(device_id)
    bg.add_task(send_notification, device_id, "Rebooted")
    return {"status": "reboot initiated"}python

WebSockets

FastAPI supports WebSocket connections natively with the same decorator style as HTTP routes. The connection is accepted with websocket.accept() and then held open in an async loop. When the client disconnects, FastAPI raises WebSocketDisconnect, giving you a clean place to run teardown logic such as removing the socket from a connection registry.

from fastapi import WebSocket, WebSocketDisconnect

@app.websocket("/ws/devices/{device_id}")
async def device_ws(websocket: WebSocket, device_id: str):
    await websocket.accept()
    try:
        while True:
            data = await websocket.receive_json()
            await websocket.send_json({"status": "ok", "echo": data})
    except WebSocketDisconnect:
        print(f"Device {device_id} disconnected")python

Settings / Config

pydantic_settings.BaseSettings reads values from environment variables (and an optional .env file) and validates them as typed fields — no manual os.getenv() calls scattered across the codebase. Field names are matched case-insensitively, so DATABASE_URL in the environment maps to database_url in the class. Keep a single settings singleton and import it wherever needed.

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str = "sqlite:///./dev.db"
    api_key: str
    debug: bool = False
    cors_origins: list[str] = ["http://localhost:3000"]

    model_config = {"env_file": ".env"}

settings = Settings()

# .env file:
# DATABASE_URL=postgresql://user:pass@localhost/dbname
# API_KEY=secret123app/config.py

Database (Async SQLAlchemy)

The async SQLAlchemy setup requires an async-compatible driver (e.g. aiosqlite for SQLite, asyncpg for Postgres) and an AsyncSession instead of the regular Session. ORM models inherit from a shared Base and map to database tables via Column declarations — equivalent to EF Core's DbSet + data annotations approach, but explicit rather than convention-based.

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from sqlalchemy import Column, Integer, String, DateTime, func

engine = create_async_engine("sqlite+aiosqlite:///./app.db")
async_session = sessionmaker(engine, class_=AsyncSession)

class Base(DeclarativeBase): pass

class Device(Base):
    __tablename__ = "devices"
    id          = Column(Integer, primary_key=True, index=True)
    serial      = Column(String(50), unique=True, nullable=False)
    name        = Column(String(100), nullable=False)
    restaurant_id = Column(String(50), nullable=False)
    status      = Column(String(20), default="offline")
    created_at  = Column(DateTime, server_default=func.now())app/database.py

Testing

FastAPI tests use httpx.AsyncClient with an ASGITransport to call the app in-process without starting a real server — equivalent to WebApplicationFactory in ASP.NET Core. The @pytest.fixture decorator creates reusable test setup, and @pytest.mark.anyio enables async test functions. Assert directly on the response's status code and parsed JSON body.

import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app

@pytest.fixture
async def client():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac

@pytest.mark.anyio
async def test_health(client: AsyncClient):
    response = await client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}tests/test_devices.py
$ pip install pytest pytest-anyio httpx $ pytest -v
06

Quick Reference

C# / Node.jsPython
var / let / constjust assign: x = 5
nullNone
Console.WriteLine() / console.log()print()
new List<T>() / [][]
Dictionary<K,V> / {}{}
.Length / .lengthlen()
foreach / for...offor x in collection:
async Task<T> / async functionasync def fn() -> T:
using / N/Awith
interface / typeProtocol or Pydantic BaseModel
[Attribute] / N/A@decorator
throwraise
catchexcept
NuGet / npmpip / uv
appsettings.json / .envpydantic_settings + .env
ASP.NET Controller / Express RouterFastAPI APIRouter
[FromBody] / req.bodyType-hint param as Pydantic model
[FromQuery] / req.queryQuery()
Middleware pipelineadd_middleware()
07

Useful One-Liners

A collection of compact Python patterns that are commonly needed in API development. The walrus operator (:=, Python 3.8+) assigns and tests a value in a single expression, and the match statement (Python 3.10+) is a structural pattern-match equivalent to C#'s switch expression — both reduce boilerplate in conditional branches.

# Check if key exists in dict
if "name" in user: ...

# Get env var with fallback
import os
db_url = os.getenv("DATABASE_URL", "sqlite:///./dev.db")

# Flatten a list of lists
flat = [item for sub in nested for item in sub]

# Dict from two lists
mapping = dict(zip(keys, vals))

# Remove duplicates preserving order
unique = list(dict.fromkeys(items))

# Safe JSON parse
import json
data   = json.loads(raw_string)       # str → dict
output = json.dumps(data, indent=2)  # dict → str

# Walrus operator (assign in expression)
if (n := len(items)) > 10:
    print(f"Too many: {n}")

# Match statement (Python 3.10+)
match status_code:
    case 200: print("OK")
    case 404: print("Not found")
    case _:   print(f"Other: {status_code}")python