Python Crash Course in 10 Minutes (Build a Real App)

2 views

Python’s reputation as a beginner language hides how much modern features matter in real code: dataclasses, type hints, match, generators, decorators, context managers and asyncio. This crash course covers them all by building one real project, a weather web app that looks up cities, shows live forecasts, saves favourites and compares five cities at once using the free Open-Meteo API. The stack matches the video: Python 3.15.1, FastAPI 0.162, Pydantic 2.14 and pytest 9.1. This written version walks through the video’s 15 chapters, with the reasoning and gotchas behind each one.

⚡ TL;DR – Always work inside a virtual environment: python -m venv .venv. – @dataclass(frozen=True) gives you immutable, comparable value objects for free. – Use raise ... from err to keep the original cause when you wrap exceptions. – Generators stream data lazily; decorators wrap behaviour like caching or timing. – FastAPI plus asyncio.gather turns five sequential API calls into one parallel wait.

Before you start

  • Python 3.12+ (the video uses 3.15.1): python3 --version.
  • A terminal and editor (VS Code with the Python extension works well).
  • No API key: Open-Meteo is free for non-commercial use.

Step 1: Variables, types, control flow and functions

python
city = "Chennai"
temps = [31.5, 29.0, 33.2]
def describe(t: float) -> str:
    if t >= 30:
        return "hot"
    elif t >= 20:
        return "warm"
    return "cool"

for t in temps:
    print(f"{city}: {t:.1f}°C is {describe(t)}")

Python is dynamically typed: names point to objects, and objects have types. f-strings with format specs (:.1f) are the cleanest way to build output.

Step 2: Collections, slicing and comprehensions

python
days = [("Mon", 31), ("Tue", 29), ("Wed", 34)]   # list of tuples
highs = {day: t for day, t in days}              # dict comprehension
hot = [d for d, t in days if t > 30]             # list comprehension
seen = {"London", "Paris", "London"}              # set -> 2 items
print(highs["Wed"], hot, days[-1], days[:2])

Use tuples for fixed records, lists for ordered collections, dicts for lookups and sets for membership. Comprehensions are faster and clearer than appending in a loop, but switch back to a loop once one stops fitting on a line or two.

Step 3: Virtual environments and pip

bash
python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install "fastapi[standard]" pytest
pip freeze > requirements.txt

A venv isolates each project’s packages, so upgrading FastAPI for one project can’t break another. Never pip install into the system Python.

Step 4: Classes and dataclasses

python
from dataclasses import dataclass, field

@dataclass(frozen=True)
class City:
    name: str
    country: str
    latitude: float
    longitude: float

    def __post_init__(self):
        if not -90 <= self.latitude <= 90:
            raise ValueError("bad latitude")

    @property
    def label(self) -> str:
        return f"{self.name}, {self.country}"

@dataclass
class Favorites:
    cities: list[str] = field(default_factory=list)

@dataclass writes __init__, __repr__ and __eq__ for you. frozen=True makes instances immutable and hashable, so they can go in sets or be dict keys. Use default_factory=list, never = []: a mutable default would be shared by every instance.

Step 5: Type hints

python
def average(xs: list[float]) -> float | None:
    return sum(xs) / len(xs) if xs else None

Hints don’t change runtime behaviour, but editors and mypy use them to catch bugs, and FastAPI and Pydantic use them to validate requests.

Step 6: Call an API with urllib

python
import json, urllib.parse, urllib.request

def geocode(name: str) -> City:
    q = urllib.parse.urlencode({"name": name, "count": 1})
    url = f"https://geocoding-api.open-meteo.com/v1/search?{q}"
    with urllib.request.urlopen(url, timeout=10) as r:
        data = json.load(r)
    if not data.get("results"):
        raise CityNotFound(name)
    c = data["results"][0]
    return City(c["name"], c.get("country", ""), c["latitude"], c["longitude"])

The standard library is enough for simple calls. Always pass timeout: without one a stalled server hangs your program forever.

Step 7: Exceptions

python
class WeatherError(Exception): ...
class CityNotFound(WeatherError):
    def __init__(self, city: str):
        super().__init__(f"No city called {city!r}")

try:
    city = geocode("Atlantis")
except CityNotFound as e:
    print(e)
except OSError as e:
    raise WeatherError("network problem") from e

Catch specific exceptions, not bare except:. raise ... from e keeps the original traceback attached, which is invaluable when debugging.

Step 8: Files with pathlib and context managers

python
from pathlib import Path
from contextlib import contextmanager
import time

FAV = Path("favorites.json")
def save(favs: list[str]) -> None:
    FAV.write_text(json.dumps(favs, indent=2), encoding="utf-8")
def load() -> list[str]:
    return json.loads(FAV.read_text(encoding="utf-8")) if FAV.exists() else []

@contextmanager
def timer(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{label}: {time.perf_counter() - start:.2f}s")

with timer("lookup"):
    geocode("London")

with guarantees cleanup even when an exception is raised. @contextmanager turns a generator into your own context manager: code before yield is setup, code in finally is teardown.

Step 9: Generators and Counter

python
from collections import Counter

def rainy_days(daily: dict):
    for date, p in zip(daily["time"], daily["precipitation_probability_max"]):
        if p >= 50:
            yield date

weekdays = Counter(date[:7] for date in rainy_days(daily))

A generator produces values one at a time instead of building a whole list in memory. Counter tallies anything iterable.

Step 10: Decorators

python
import functools

def retry(times: int = 3):
    def wrap(fn):
        @functools.wraps(fn)
        def inner(*args, **kwargs):
            for attempt in range(times):
                try:
                    return fn(*args, **kwargs)
                except OSError:
                    if attempt == times - 1:
                        raise
                    time.sleep(2 ** attempt)
        return inner
    return wrap

@functools.lru_cache(maxsize=256)
@retry()
def cached_geocode(name: str) -> City: return geocode(name)

A decorator is a function that takes a function and returns a new one. functools.wraps keeps the original name and docstring. lru_cache works here because City is frozen and hashable.

Step 11: match statements

python
def describe_code(code: int) -> str:
    match code:
        case 0: return "clear"
        case 1 | 2 | 3: return "partly cloudy"
        case c if 61 <= c <= 67: return "rain"
        case _: return "other"

match does structural pattern matching: it can match literals, alternatives, guards and even the shape of dicts and dataclasses, not just values.

Step 12: FastAPI

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()
class Day(BaseModel):
    date: str
    max: float
    min: float

@app.get("/api/forecast/{name}")
async def forecast(name: str) -> list[Day]:
    try:
        city = await asyncio.to_thread(cached_geocode, name)
    except CityNotFound as e:
        raise HTTPException(404, str(e))
    daily = await fetch_daily(city)
    return [Day(date=t, max=a, min=b) for t, a, b in
            zip(daily["time"], daily["temperature_2m_max"], daily["temperature_2m_min"])]
bash
fastapi dev main.py     # http://127.0.0.1:8000/docs for interactive API docs

The type hints drive validation and the auto-generated /docs page. asyncio.to_thread keeps blocking urllib code from freezing the event loop.

Step 13: asyncio for parallel lookups

python
@app.get("/api/compare")
async def compare(cities: str = "Chennai,London,Tokyo,Paris,Toronto"):
    names = cities.split(",")[:5]
    results = await asyncio.gather(*(forecast(n) for n in names), return_exceptions=True)
    return {n: (r if not isinstance(r, Exception) else {"error": str(r)})
            for n, r in zip(names, results)}

gather runs the five lookups concurrently, so the total time is roughly the slowest one. return_exceptions=True stops one bad city from failing the whole response.

Step 14: Testing with pytest

python
# test_app.py
from fastapi.testclient import TestClient
from main import app, describe

def test_describe():
    assert describe(31) == "hot"

def test_unknown_city(monkeypatch):
    def boom(name): raise CityNotFound(name)
    monkeypatch.setattr("main.cached_geocode", boom)
    r = TestClient(app).get("/api/forecast/Atlantis")
    assert r.status_code == 404
bash
pytest -q

Plain assert statements and monkeypatch to replace network calls keep tests fast and deterministic.

Troubleshooting

  • ModuleNotFoundError: fastapi: the venv isn’t activated, or you installed into a different Python.
  • TypeError: unhashable type with lru_cache: an argument or dataclass isn’t frozen.
  • The server freezes under load: blocking I/O inside async def. Wrap it in asyncio.to_thread or use an async HTTP client.
  • UnicodeDecodeError on Windows: always pass encoding="utf-8" to file reads and writes.

Best practices

  • One venv per project, with pinned requirements.
  • Frozen dataclasses for value objects, Pydantic models at API boundaries.
  • Specific exceptions, chained with from.
  • Timeouts on every network call; tests that never hit the network.

Edge cases and a production-ready touch

Mutable default arguments. The classic Python bug, and why Step 4 uses default_factory:

python
def add(city, favs=[]):      # the same list is reused on every call!
    favs.append(city); return favs
add("A"); print(add("B"))    # ['A', 'B']
def add(city, favs=None):    # fix
    favs = [] if favs is None else favs

Atomic file writes. If the process dies halfway through write_text, favorites.json is corrupted. Write to a temporary file, then rename it, which is atomic on the same filesystem:

python
def save(favs: list[str]) -> None:
    tmp = FAV.with_suffix(".tmp")
    tmp.write_text(json.dumps(favs, indent=2), encoding="utf-8")
    tmp.replace(FAV)

Floating-point surprises. 0.1 + 0.2 == 0.3 is False. Compare with math.isclose, and round only for display.

Validate query input. Let FastAPI do it with Query:

python
from fastapi import Query
@app.get("/api/search")
async def search(q: str = Query(min_length=2, max_length=80)): ...

Invalid input now returns a 422 with a clear message, before your code runs. Finally, run the app with fastapi run main.py (not dev) in production, behind a reverse proxy.

Logging instead of print. Use the logging module, so you can control levels and output in production without editing code:

python
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("weather")
log.info("geocoded %s", city.label)

FAQ

FastAPI or Flask?

FastAPI if you want async, automatic validation and docs. Flask is fine for small synchronous apps.

Do type hints slow Python down?

No. They’re ignored at runtime unless a library such as Pydantic reads them.

Official documentation

Key takeaways

  • Use venvs, dataclasses and type hints from day one.
  • Context managers and decorators package setup, cleanup and cross-cutting behaviour.
  • Generators keep memory flat.
  • FastAPI plus asyncio make a concurrent JSON API in a few dozen lines, and pytest keeps it honest.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

PDF for this post is being prepared. Subscribe now and we’ll unlock downloads sitewide as packs go live.

Subscribe free →


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.