Your first script fails. Not because the logic is wrong, but because you typed `pritn` instead of `print`. This is normal. Every engineer who now calls GPT-4 or Claude from code started exactly here, staring at a red error message that made no sense. Python is the language of AI engineering because it is readable, has a massive ecosystem of AI libraries, and lets you go from an idea to a working prototype in minutes. But there is a gap worth naming honestly up front: knowing enough Python to write a script is not the same as knowing enough Python to build a RAG pipeline, an agent, or a production API. This module targets the second bar, not the first. By the end you will be comfortable with the actual Python that shows up in AI codebases: typed function signatures, comprehensions, generators for streaming, async calls to model APIs, structured config, and tests that actually catch failures instead of hiding them. ---
### Variables, strings, and numbers A **variable** is a labelled box that holds a value. Instead of typing `25` everywhere, you store it once under a name and reuse that name. ```python learner_name = "Priya Sharma" ## a string - text data learner_age = 25 ## an integer - whole number course_progress = 42.5 ## a float - decimal number is_certified = False ## a boolean - True or False only ``` Strings matter more in AI engineering than in most programming, because prompts, documents, and model responses are all text. ```python company_name = "Swiggy" greeting = f"Welcome to {company_name} support, how can I help?" ## f-strings insert variables directly into text using {} ``` > 🔴 **Common Mistake:** Treating text read from user input or an API > response as a number. `"25" + 5` crashes. Convert explicitly first with > `int("25")` before doing any math on it. ### Lists, tuples, sets, and dictionaries - when to use which Python gives you four core collection types, and picking the wrong one is a common source of confusing bugs later on. A **list** is ordered and changeable, use it for sequences you will modify. ```python support_tickets = ["Refund delay", "App crash", "Payment failed"] support_tickets.append("Login issue") ## lists can grow after creation ``` A **tuple** is ordered but fixed once created, use it for values that should never change, like coordinates or a fixed configuration pair. ```python model_config = ("claude-sonnet-5", 4096) ## (model_name, max_tokens) ## model_config[0] = "gpt-4" would raise an error - tuples are immutable ``` A **set** stores unique values with no order, use it to remove duplicates or check membership quickly. ```python seen_document_ids = {"doc_101", "doc_102"} seen_document_ids.add("doc_101") ## no effect, it is already present print(len(seen_document_ids)) ## 2 - duplicates are automatically dropped ``` A **dictionary** stores key-value pairs, use it whenever your data has labels. This matters enormously in AI engineering because almost every API response you receive comes back shaped like a dictionary. ```python api_response = { "model": "claude-sonnet-5", "tokens_used": 142, "success": True } print(api_response["tokens_used"]) ## 142 ``` > 📌 **Remember:** List for an ordered, changeable sequence. Tuple for fixed > data that should not change. Set for uniqueness and fast lookups. > Dictionary for labelled data, which is what most AI API responses look like. | Type | Ordered | Changeable | Typical AI use | |:---|:---|:---|:---| | list | Yes | Yes | Chunks, retrieved documents, message history | | tuple | Yes | No | Fixed config pairs, coordinates | | set | No | Yes | Deduplicating chunk IDs, seen items | | dict | Yes (insertion) | Yes | API responses, structured records | ---
### List and dictionary comprehensions A **comprehension** builds a new list or dictionary in a single readable line instead of a multi-line loop. AI and data code is full of these. ```python documents = ["Refund policy", "", "Return process", ""] ## Instead of a loop with an if-check and .append(), write it in one line non_empty_docs = [doc for doc in documents if doc != ""] print(non_empty_docs) ## ["Refund policy", "Return process"] ``` ```python ## Dictionary comprehension - building a lookup from a list doc_lengths = {doc: len(doc) for doc in non_empty_docs} print(doc_lengths) ## {"Refund policy": 13, "Return process": 14} ``` > **Note:** A comprehension is just a compact `for` loop. If a comprehension > starts needing multiple conditions or nested loops to the point it is hard > to read, switch back to a regular loop. Readability wins over cleverness. ### Lambda functions, *args, and **kwargs A **lambda** is a small, unnamed function written inline, most often used as a short sort key or callback. ```python results = [ {"chunk": "Order tracking info", "score": 0.72}, {"chunk": "Refund policy details", "score": 0.91} ] ## Sort by score, highest first - the lambda extracts the sort key results.sort(key=lambda item: item["score"], reverse=True) print(results[0]["chunk"]) ## "Refund policy details" ``` `*args` collects extra positional arguments, `**kwargs` collects extra named arguments. You may encounter these in Python libraries and framework code where a function needs to accept optional named arguments. ```python def call_model(prompt, *args, **kwargs): """ prompt is required. *args catches any extra positional values. **kwargs catches any extra named options like temperature or model. """ print(f"Prompt: {prompt}") print(f"Extra options: {kwargs}") call_model("Summarise this ticket", temperature=0.2, model="claude-sonnet-5") ## Output: Extra options: {'temperature': 0.2, 'model': 'claude-sonnet-5'} ``` > 💡 **Tip:** Recognising the `**kwargs` pattern when you read it matters > more than writing it yourself early on. Modern typed AI SDKs often prefer > explicit parameters or config objects instead, so do not expect to see it > everywhere. ---
### Why yield matters for streaming A normal function returns once and stops. A **generator** uses `yield` to produce values one at a time, pausing between each one. This is the exact mechanism behind streaming AI responses, where tokens arrive one at a time instead of all at once. ```python def generate_chunks(documents): """Yield one document at a time instead of building a full list first.""" for document in documents: yield document ## pauses here, resumes on the next request for chunk in generate_chunks(["Doc A", "Doc B", "Doc C"]): print(f"Processing: {chunk}") ``` > **Note:** A regular function with `return` gives you everything at once > and uses memory for the whole result. A generator with `yield` gives you > one item at a time, which is why streaming APIs and large-file processing > both rely on this pattern. ### A simple streaming-style example ```python def stream_response_tokens(full_text): """Simulate streaming a response word by word instead of all at once.""" for word in full_text.split(): yield word + " " for token in stream_response_tokens("Your refund will be processed soon"): print(token, end="") ## prints word by word, simulating a live stream ``` Generators provide the same core programming idea used in streaming, producing values incrementally instead of waiting for the complete result. Real AI SDKs may expose streaming through generators, async iterators, or provider-specific streaming APIs built on HTTP mechanisms like Server-Sent Events, `yield` is one way to think about the pattern, not the only mechanism a real streaming implementation uses. ---
### Functions with type hints A **function** is a named, reusable block of code. A **type hint** tells readers and tools what type each parameter and return value should be, without changing how Python actually runs. ```python def calculate_refund(order_amount: float, days_delayed: int) -> float: """ Calculate refund amount based on order value and delay. Orders delayed more than 5 days get a 10% bonus credit. """ refund = order_amount if days_delayed > 5: refund = refund + (order_amount * 0.10) return refund final_refund = calculate_refund(order_amount=500, days_delayed=7) print(final_refund) ## 550.0 ``` > 📌 **Remember:** Type hints do not stop Python from running mismatched > types, they are not enforced automatically. Their real value is readability > and tooling: your editor can catch mistakes early, and they make the jump > to Pydantic models later in this path far more natural. ### Structuring a project into modules and packages A single growing file becomes unmanageable fast. A real AI project is organised into separate files, each with a clear job. app/ main.py services/ llm_client.py models.py tests/ test_llm_client.py ```python ## Inside services/llm_client.py def call_model(prompt: str) -> str: """Send a prompt to the model and return its text response.""" return f"Response to: {prompt}" ``` ```python ## Inside main.py - importing from another file (a module) from services.llm_client import call_model result = call_model("Summarise this ticket") print(result) ``` > **Note:** A **module** is any single `.py` file you can import from. A > **package** is a folder of modules with an `__init__.py` file marking it > as importable. Splitting code this way is what lets a team work on > `llm_client.py` without touching `main.py`. ### Dataclasses for structured data A **dataclass** is a shortcut for creating a class whose main job is holding structured data, without writing repetitive boilerplate. ```python from dataclasses import dataclass @dataclass class Document: text: str source: str score: float retrieved = Document(text="Refund policy", source="hr_docs", score=0.91) print(retrieved.score) ## 0.91 ``` > 💡 **Tip:** Reach for a dataclass whenever you are passing structured > records around, like retrieved chunks, tool call results, or evaluation > scores. This is the exact shape of thinking Pydantic models build on later. ---
### Environment variables and .env files An **environment variable** is a value set outside your code, at the operating system or deployment level, so secrets never live in source files. ```python import os api_key = os.environ.get("AI_API_KEY") if api_key is None: raise ValueError("AI_API_KEY environment variable is not set") ``` In local development, these are commonly stored in a `.env` file and loaded with a library like `python-dotenv`. ```text AI_API_KEY=your-api-key-here ENVIRONMENT=development ``` ```python from dotenv import load_dotenv import os load_dotenv() ## reads the .env file and loads it into os.environ api_key = os.environ.get("AI_API_KEY") ``` > ⚠️ **Security:** Add `.env` to your `.gitignore` immediately, before your > first commit. A key pushed to a public GitHub repository by accident gets > found and abused within minutes, sometimes seconds. ### Development versus production configuration ```python import os environment = os.environ.get("ENVIRONMENT", "development") if environment == "production": model_name = "claude-sonnet-5" request_timeout = 10 else: model_name = "claude-haiku-4-5" ## cheaper model while developing request_timeout = 30 ## more patience locally ``` > 🔴 **Common Mistake:** Hardcoding a single configuration for every > environment. Development needs cheaper models and longer timeouts for > debugging, production needs stricter timeouts and cost-aware defaults. ---
Your first script fails. Not because the logic is wrong, but because you typed pritn instead of print. This is normal. E...
Variables, strings, and numbers A variable is a labelled box that holds a value. Instead of typing 25 everywhere, you st...
List and dictionary comprehensions A comprehension builds a new list or dictionary in a single readable line instead of ...
Why yield matters for streaming A normal function returns once and stops. A generator uses yield to produce values one a...
Functions with type hints A function is a named, reusable block of code. A type hint tells readers and tools what type e...
Environment variables and .env files An environment variable is a value set outside your code, at the operating system o...
Reading and writing files and JSON Most AI APIs use JSON for request and response bodies, making JSON handling an essent...
Synchronous versus asynchronous execution A synchronous call blocks your program until it finishes. If you call three AI...
Provider-specific schemas, not one universal shape This example intentionally does not target a real provider. The goal ...
pytest fundamentals Basic mocking for API calls Real tests should not make real network calls, they should be fast and p...
Build a small pipeline that mirrors real AI engineering work: read structured input, convert it into typed records, call...
Concept Syntax When to use List comprehension [x for x in items if cond] Build a filtered/transformed list in one line G...
Using a bare except Exception: around an API call hides the real failure reason and makes debugging production incidents...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.