Lesson 1 — Beginner
Python for AI Engineering
AI engineering ka zyadatar code Python mein hota hai — API clients se lekar data scripts aur test harness tak. Is lesson mein language ki asli taakat aur asli traps dono dekhoge: idiomatic Python, data structures, type hints, mutability, files aur env, async, uv aur pytest — sab ek chhote multi-file project ke nazar se.
AI engineering mein Python ki kya zarurat padti hai
WHAT
Python ek high-level language hai jiska syntax padhne mein aasaan hai aur standard library rich hai. AI engineering mein iska matlab — data jodna, API call karna, result parse karke aage bhejna, ye sab kam lines mein ho jaata hai.
WHEN
Jab bhi kaam logic + external systems ka ho — API client, data transform, batch script, test harness — Python first choice hai. AI/ML libraries ka ecosystem bhi isi language ka hai.
WHERE
FastAPI services, data pipelines, CLI tools, notebooks aur har dusre AI tool ke wrappers mein Python milega. Is course ke baaki levels ka bhi zyadatar code Python hi hoga.
Funda: Python mein likhna aasaan hai, lekin galat likhna bhi utna hi aasaan. Dynamic typing aur shared references tumhe freedom dete hain — aur wahi freedom se bugs aate hain. Isliye is lesson ka focus sirf syntax nahi, sahi tareeqa hai kaam karna.
Clean, idiomatic Python
from statistics import mean
def grade_students(scores: dict[str, int]) -> dict[str, str]:
"""Har student ko score se grade do."""
return {name: ("pass" if s >= 40 else "fail") for name, s in scores.items()}
def class_average(scores: dict[str, int]) -> float:
return mean(scores.values())
class Roster:
def __init__(self) -> None:
self.students: dict[str, int] = {}
def add(self, name: str, score: int) -> None:
self.students[name] = score
def __len__(self) -> int:
return len(self.students)
roster = Roster()
roster.add("Aarti", 78)
print(len(roster), grade_students(roster.students), class_average(roster.students))
- Functions chhote rakho: ek function = ek kaam, aur naam se hi bolna chahiye wo kya karta hai (grade_students, fetch_orders).
- Classes tabhi jab state ho: koi mutable state sambhalni ho (Roster, Cache) tab class — warna simple function hi kaafi hai.
- Comprehensions: list/dict/set banane ka Python wala tareeqa. Lekin do se zyada nest karoge toh normal loop hi padhne layak rehta hai.
- Exceptions: galat input par raise karo, False/None return karke chhupao mat — caller ko clarity milti hai ki kya toot gaya.
- Modules: har kaam ki alag file — clients.py, grader.py, main.py. Ek hi 800-line script baad mein roya deta hai.
Core data structures — kaunsa kab
# LIST — ordered, mutable (sequence jo badalta rehta hai)
tags = ["rag", "agents", "evals"]
tags.append("fine-tuning")
# TUPLE — ordered, immutable (fixed record)
point = (12, 9)
# SET — unique members, membership O(1) jaisa fast
seen = {"u1", "u2"}
seen.add("u1")
print(len(seen)) # 2 — duplicate gayab
# DICT — key se value dhoondo (lookup ka workhorse)
prices = {"gpu": 40, "cpu": 4}
print(prices.get("tpu", 0)) # missing key pe default, crash nahi
# GENERATOR — lazy sequence, memory bachti hai
def chunks(text: str, size: int):
for i in range(0, len(text), size):
yield text[i:i + size]
print(list(chunks("abcdefgh", 3))) # ["abc", "def", "gh"]
- List vs tuple: list = badalne wala sequence, tuple = fixed record. Function ko tuple do agar usse change nahi karna.
- Set: uniqueness aur membership check (x in seen) ke liye. Wahi check list par scan karta hai — O(n) vs O(1), data badhte hi farq dikhta hai.
- Dict: har lookup-heavy kaam ka default. Counter aur setdefault jaise helpers aksar kaam aate hain.
- Iterator vs generator: iterator ek cursor hai, generator function se bana lazy sequence (yield). File lines, API pages jaise chalte hue data pe generator hi chahiye.
- Complexity soch: data structure ka chunav access pattern se hota hai — membership, order, uniqueness, key lookup. Yaad nahi, reason karo.
Type hints, models aur static analysis
from dataclasses import dataclass
from pydantic import BaseModel, Field, ValidationError
# Type hints — runtime pe rukta nahi, tools isse padh kar galti pakadte hain
def fetch_user(user_id: str, timeout: float = 5.0) -> dict[str, str]: ...
# dataclass — chhota fixed record (init/repr/eq khud ban jaate hain)
@dataclass(frozen=True)
class Token:
access: str
expires_in: int
# Pydantic — bahar se aaya data validate karo (API body, JSON, config)
class UserIn(BaseModel):
name: str = Field(min_length=1)
email: str
age: int = Field(default=18, ge=0, le=120)
payload = {"name": "Ravi", "email": "ravi@example.com", "age": -3}
try:
UserIn.model_validate(payload)
except ValidationError as exc:
print("validation failed:", exc.errors()[0]["msg"])
- Type hints ka asli faayda runtime nahi, static analysis hai: mypy ya pyright chalao — galat argument type ka error mil jaata hai, run karne se pehle.
- dataclass: sirf data wale chhote classes ke liye — boilerplate zero.
- Pydantic: jab data bahar se aaye (HTTP body, JSON file, env config) — trust mat karo, validate karo. Model sahi hua toh aage ka code safe.
- Typed interface = contract: signature hi bata deta hai kya andar aayega, kya bahar jaayega — reviewer ko poora file padhna nahi padta.
Mutability, scope aur common failure modes
def add_tag_galat(tags=[]): # GALAT — default list sirf ek baar banti hai
tags.append("new")
return tags
print(add_tag_galat()) # ["new"]
print(add_tag_galat()) # ["new", "new"] — purana state wapas!
def add_tag(tags: list[str] | None = None): # SAHI
tags = tags if tags is not None else []
tags.append("new")
return tags
# Reference vs copy — sabse common surprise
counts = [1, 2, 3]
alias = counts # alag list nahi — wahi object ka doosra naam
alias.append(4)
print(counts) # [1, 2, 3, 4] — counts bhi badal gaya
snapshot = counts.copy() # ab alag copy
# Context manager — block chhote hi resource release
with open("app.log", "a") as fh:
fh.write("boot line")
- Mutable default argument Python ka classic trap hai — default definition ke waqt ek baar banta hai aur har call par wahi shared object milta hai.
- Reference, copy nahi: b = a se nayi list nahi banti. Nested data par shallow copy (.copy()) aur deep copy (copy.deepcopy()) ka farq dikhta hai.
- Scope: module-level names global hain, function ke andar ke local. Module-level mutable state par sab log likhte hain toh bug dhoondna mushkil — state ko parameter ya class se pass karo.
- Exceptions: except ke andar khaali pass mat chhodo — kam se kam log karo. Bina exception type ke bare except KeyboardInterrupt tak kha jaata hai.
- Debugging order: pehle pura traceback padho (line + exception type), phir logging se values nikalo, phir chhota reproducer banao. Stack trace chhodna sabse badi beginner galti hai.
Files, paths, environment variables aur JSON
import csv
import json
import os
from pathlib import Path
# Pathlib — OS-agnostic paths (string se mat jodna)
data_dir = Path("data") / "raw" # data/raw — Windows ho ya Linux
data_dir.mkdir(parents=True, exist_ok=True)
# JSON — dict <-> string (har API response ka format)
report = {"runs": 3, "best_score": 0.91}
(data_dir / "report.json").write_text(json.dumps(report, indent=2))
loaded = json.loads((data_dir / "report.json").read_text())
# CSV — header se fields aate hain
with (data_dir / "runs.csv").open("w", newline="") as fh:
writer = csv.DictWriter(fh, fieldnames=["run", "score"])
writer.writeheader()
writer.writerow({"run": 1, "score": 0.87})
# Environment variables — config code ke bahar
port = int(os.environ.get("PORT", "8000"))
db_url = os.environ["DATABASE_URL"] # zaroori ho toh missing par clear error
- Pathlib: "dir" + "/" + file wali string concatenation se bacho — pathlib separator handle karta hai aur exists(), read_text() jaise helpers deta hai.
- JSON: json.dumps (dict to string) aur json.loads (string to dict); file ke liye dump/load. API se aaya har response JSON hi hai — yehi do functions har jagah chalte hain.
- CSV: DictReader/DictWriter — header se field names aate hain, column ke naam se kaam hota hai.
- Env vars: PORT, DATABASE_URL, API keys — code ke bahar. get(key, default) se chhote options safe, zaroori var ko missing par hi fail karo.
- Bade config ke liye: Pydantic Settings jaisa pattern — string to int coercion aur validation ek saath ho jaata hai.
Sync vs async — concurrent API calls
import asyncio
import time
import httpx
URLS = [f"https://api.example.com/items/{i}" for i in range(5)]
# SYNC — ek ek karke, har call doosri ka wait karti hai
def fetch_all_sync() -> list:
with httpx.Client() as client:
return [client.get(u).json() for u in URLS]
# ASYNC — sab calls lagti hain, wait ek saath hota hai
async def fetch_all_async() -> list:
async with httpx.AsyncClient() as client:
tasks = [client.get(u) for u in URLS]
return [r.json() for r in await asyncio.gather(*tasks)]
async def main() -> None:
start = time.perf_counter()
await fetch_all_async()
print("async done in", round(time.perf_counter() - start, 2), "s")
asyncio.run(main())
Sync ka matlab: ek kaam khatam hone tak baitho. Async ka matlab: ek thread ke andar bahut saare I/O wait ko overlap karo. Network I/O (API calls, DB queries) mein zyadatar waqt wait hota hai — wahi wait async se overlap hota hai. CPU-heavy kaam (image resize, model inference) async se tez nahi hota — uske liye process/thread chahiye.
- async def sirf tabhi jab andar await ho — poora call chain sync reh gayi toh ek ruka hua function sab block kar dega.
- await ka matlab "yahan ruko jab tak ye ho jaaye" — is waqt event loop doosre tasks chalata rehta hai.
- Async context mein async client: httpx.AsyncClient ya aiohttp. requests library sync hai — async function mein use kiya toh poora loop ruk jaayega.
- Timeout hamesha do: bina timeout ke ek hang request tumhara kaam ke liye hamesha ke liye atak sakti hai.
uv, packages aur pytest — project ko pakka banao
# uv — environment + dependency manager (pip + venv + poetry ka kaam)
uv init my-service
uv add fastapi httpx # dependencies (pyproject + lock update)
uv add --dev pytest ruff # sirf development ke liye
uv run pytest -q # environment mein hi test chalao
uv run python -m my_service
# pyproject.toml (uv ne banaya — commit karo)
[project]
name = "my-service"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["fastapi>=0.115", "httpx>=0.27"]
[dependency-groups]
dev = ["pytest>=8", "ruff>=0.6"]
# uv.lock — exact versions + hashes (bhi commit karo)
import pytest
from my_service.grader import grade
def test_pass_boundary():
assert grade(40) == "pass"
def test_fail_below_boundary():
assert grade(39) == "fail"
def test_rejects_negative_score():
with pytest.raises(ValueError):
grade(-1)
# chalao: uv run pytest -q
- Package layout: my_service/ (code) + tests/ + pyproject.toml + uv.lock. Sab kuch ek script mein mat likho — import structure bata deta hai kaunsa module kya karta hai.
- uv.lock commit karo: uske bina teammate ka install naya version la sakta hai aur "mere paas chal raha hai" wali problem shuru. Lock file se environment reproducible banta hai.
- pytest style: test_xxx functions, assert, aur pytest.raises se failure cases. Har test ek behaviour — naam hi batata hai kya check ho raha hai.
- Debugging: traceback padho, phir logging/print se values lao, phir breakpoint se step-through. Ye order har language mein kaam karta hai.
Exercise
Question: Function ke parameters aur return value ka type batane wala syntax kya kehlata hai? (do shabd)
Question: Do sequences mein se kaunsa immutable hai — list ya tuple?
Question: Python project ke dependencies ka exact version lock karne wala file ka naam? (extension ke saath)
Question: Is lesson mein tests chalane ke liye jo tool use hua uska naam?
Common mistakes
- Mutable default argument likhna: def f(tags=[]) mein list definition par ek baar banti hai aur har call par share hoti hai — purana state wapas aata hai. Fix: default None rakho aur andar fresh list banao.
- b = a ko copy samajhna: Assignment se nayi list nahi banti, sirf doosra naam banta hai — mutate kiya toh dono badlenge. Fix: .copy() / copy.deepcopy() ya immutable tuple.
- Async function mein sync client rakhna: async def ke andar requests.get() daaloge toh event loop ruk jaayega aur ek slow upstream poora server block karega. Fix: httpx.AsyncClient + await, ya kaam ko sync rakho.
- Sab kuch ek hi script mein likhna: Ek file mein 800 lines se test lagana aur reuse dono mushkil ho jaate hain. Fix: modules/packages banao, uv se environment lock karo, pytest se har module ka test likho.
Thaila (list) mein jo daalo wo badal sakta hai, aur do logon ki "thaila" ek hi nikli toh dono ka saman aapas mein jud jaata hai. Seal-pack dabba (tuple) khulta hi nahi — isliye fixed record usme rakho:
thaila = ["pyaz", "aloo"]
dusri = thaila # alag thaila nahi — wahi hai
dusri.append("mirch")
print(thaila) # pyaz, aloo, MIRCH — dono mein mirch aa gayi
dabba = ("namak", "haldi") # seal-pack
# dabba.append("mirch") # TypeError — andar kuch badal nahi sakta
# Shared state ho toh copy karo, fixed record ho toh tuple rakho.- Q: list aur tuple ka farq kya hai aur kaunsa kab use karte hain?
- Q: Type hints runtime pe errors kyun nahi rokte, phir bhi kyun likhte hain? — static analysis (mypy/pyright) ke liye.
- Q: async/await se performance kis type ke kaam mein badhti hai aur kyun? — I/O wait ko overlap karke.
- Q: pyproject.toml aur uv.lock ka role kya hai? — declared dependencies vs locked, reproducible environment.
- Q: Program chalta hai par output galat hai — debugging ka order kya hoga? — traceback, logging, minimal repro.
Python ka foundation taiyar. Agla lesson — The Web: HTTP, REST & JSON — mein dekhenge tumhara code internet pe bahar ke systems se kaise baat karta hai.