AI Tutorial
Home All Courses Blog Interview Prep ↗ 0%
1
Curious builder 0 XP earned · 300 to level 2
0 days Finish a lesson to begin
Badge collection
0 of 5 unlocked
small wins to finish your path
Next lesson →

Types, functions, and modules

Every AI script, no matter how advanced, is built from the same small set of ideas: values of different types, decisions and loops, functions that transform data, errors handled gracefully, and modules that organise code into files. This lesson covers each of them properly, with the AI-flavoured examples you will actually write.

Last updated: 01 Nov, 2026 · Python 3.11+

Variables and dynamic typing

Definition · Variable

A name that refers to a value stored in memory. You create one with =, for example model = "gpt-4o-mini". The name is a label; the value is the actual data.

Definition · Type

The kind of value — whole number, decimal, text, true/false, list, and so on. The type decides which operations are allowed: you can add two numbers, but you cannot divide a piece of text.

Python is dynamically typed: you never declare a type in advance. The value carries its type, and the same name can later refer to a value of a different type. Python is also strongly typed: it will not silently convert between unrelated types — "3" + 3 is an error, not "33" or 6.

Python
name = "Rahish"           # str   - text
score = 0.92              # float - decimal number
count = 3                 # int   - whole number
ready = True              # bool  - True or False
answer = None             # None  - "no value yet"

print(type(name).__name__, type(score).__name__, type(count).__name__)
print(type(ready).__name__, type(answer).__name__)
str float int bool NoneType

Variable names use snake_case by convention: lowercase words separated by underscores, such as max_tokens or chunk_size. Names are case-sensitive, cannot start with a digit, and should describe what they hold.

Numbers: int and float

Definition · int and float

int stores whole numbers of any size (42, -7, 10_000_000). float stores decimal numbers using 64-bit floating point (0.75, 1e-5). Model scores, probabilities, and temperatures are floats; counts and token limits are ints.

OperatorMeaningExampleResult
+ - *Add, subtract, multiply3 * 412
/True division — always returns a float7 / 23.5
//Floor division — rounds down to a whole number7 // 23
%Remainder (modulo)7 % 21
**Power2 ** 101024
Python
print(7 / 2, 7 // 2, 7 % 2, 2 ** 10)
print(0.1 + 0.2)
print(round(0.1 + 0.2, 2))
3.5 3 1 1024 0.30000000000000004 0.3
Watch out

Floats are binary approximations, so 0.1 + 0.2 is not exactly 0.3. Never compare floats with == — use round() or math.isclose(a, b). This matters when you compare similarity scores or evaluation metrics.

Strings: working with text

Definition · String (str)

An immutable sequence of Unicode characters, written in single or double quotes. “Immutable” means string methods never change the original; they return a new string. Prompts, documents, and model responses are all strings.

f-strings for building prompts

An f-string (formatted string literal) starts with f and lets you embed any expression inside {}. Format specifiers after a colon control how values are displayed.

Python
model = "gpt-4o-mini"
temperature = 0.2
max_tokens = 1500

prompt = f"Model: {model} | temp={temperature:.1f} | tokens={max_tokens:,}"
print(prompt)
Model: gpt-4o-mini | temp=0.2 | tokens=1,500

Triple-quoted strings span multiple lines, which is ideal for prompt templates:

Python
context = "Paris is the capital of France."
question = "What is the capital of France?"

prompt = f"""Answer using only the context.

Context: {context}
Question: {question}
Answer:"""
print(prompt)
Answer using only the context. Context: Paris is the capital of France. Question: What is the capital of France? Answer:

Essential string methods

Python
text = "  RAG Retrieves Context  "
print(text.strip().lower())                    # trim + lowercase
print("a,b,c".split(","))                      # split into a list
print(" | ".join(["rag", "agents", "evals"]))  # join a list into one string
print("chunk_001.txt".startswith("chunk"), "notes.md".endswith(".txt"))
print("Hello AI"[0:5], "Hello AI"[-2:])        # slicing
rag retrieves context ['a', 'b', 'c'] rag | agents | evals True False Hello AI
MethodWhat it does
s.strip()Remove whitespace from both ends (lstrip/rstrip for one side).
s.lower() / s.upper()Change case — useful for case-insensitive matching.
s.split(sep)Split into a list; with no argument, splits on any run of whitespace.
sep.join(items)Combine a list of strings with sep between them.
s.replace(a, b)Replace every occurrence of a with b.
s.startswith(x) / s.endswith(x)Check a prefix or suffix.
x in sCheck whether substring x appears in s.
s[start:end]Slice characters; negative indexes count from the end.

Booleans, None, and truthiness

Definition · bool and None

bool has exactly two values, True and False, produced by comparisons such as score > 0.5. None is a special value meaning “nothing here” — for example a function that found no result, or an optional setting that was not provided.

In an if statement, every value counts as either truthy or falsy. Empty and zero-like values are falsy; almost everything else is truthy.

Python
for value in [0, 0.0, "", [], {}, None, "0", [0]]:
    print(repr(value), bool(value))
0 False 0.0 False '' False [] False {} False None False '0' True [0] True

This is why if not text: is the idiomatic way to skip empty documents. To check specifically for None, use is: if result is None:.

Comparison / logicMeaning
== !=Equal / not equal
< <= > >=Ordering comparisons
and or notCombine or negate conditions
is / is notIdentity — use for None, True, False
in / not inMembership in a string, list, set, or dict keys

Converting between types

Data from files, environment variables, and HTTP responses often arrives as text. Convert it explicitly with the type's name:

Python
print(int("42") + 1, float("0.75") * 2, str(3) + "3", bool(""))

try:
    int("abc")
except ValueError as e:
    print("ValueError:", e)
43 1.5 33 False ValueError: invalid literal for int() with base 10: 'abc'

Control flow: decisions and loops

if / elif / else

Python uses indentation (four spaces) to define blocks. There are no curly braces.

Python
score = 0.74

if score >= 0.8:
    label = "relevant"
elif score >= 0.5:
    label = "maybe"
else:
    label = "irrelevant"

print(label)
maybe

for loops

A for loop runs once for each item in any iterable — a list, a string, the lines of a file, or a range of numbers.

Python
docs = ["intro to rag", "", "vector databases", "evals"]

for doc in docs:
    if not doc:
        continue          # skip empty strings, go to the next item
    print(doc.title())

for i in range(3):        # 0, 1, 2
    print("attempt", i + 1)
Intro To Rag Vector Databases Evals attempt 1 attempt 2 attempt 3

while loops

A while loop repeats as long as a condition is true. It is common for retry logic. break leaves the loop early.

Python
attempt = 0
succeeded = False

while attempt < 5:
    attempt += 1
    if attempt == 3:          # pretend the third call works
        succeeded = True
        break

print(f"succeeded={succeeded} after {attempt} attempts")
succeeded=True after 3 attempts

Functions

Definition · Function

A named, reusable block of code that takes inputs (parameters), performs work, and optionally sends back a result with return. A function that reaches the end without return returns None.

Functions are the main unit of AI code: clean_text(), chunk_document(), embed(), ask_model(). Good functions do one job, have a clear name, and are easy to test.

Python
def normalize(text):
    """Lowercase text and collapse repeated whitespace."""
    return " ".join(text.lower().split())

print(normalize("  Hello   RAG  "))
hello rag

The string directly under the def line is a docstring. Editors show it on hover, and help(normalize) prints it. Write one for every function you share.

Parameters, defaults, and keyword arguments

Python
def build_prompt(question, context="", style="concise"):
    prompt = f"Answer in a {style} way.\n"
    if context:
        prompt += f"Context: {context}\n"
    return prompt + f"Question: {question}"

print(build_prompt("What is RAG?"))
print("---")
print(build_prompt("What is RAG?", style="detailed", context="RAG = retrieval + generation"))
Answer in a concise way. Question: What is RAG? --- Answer in a detailed way. Context: RAG = retrieval + generation Question: What is RAG?
  • Positional arguments are matched by order: "What is RAG?" becomes question.
  • Default values (style="concise") make a parameter optional.
  • Keyword arguments (style="detailed") are matched by name, so order does not matter and calls are self-documenting. Prefer them for settings like temperature or max_tokens.

Returning multiple values

Return several values separated by commas; Python packs them into a tuple, and the caller can unpack them into separate names.

Python
def text_stats(text):
    words = text.split()
    return len(words), len(text)

word_count, char_count = text_stats("embeddings map text to vectors")
print(word_count, char_count)
5 30

*args and **kwargs

*args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dictionary. You will see them constantly in library code that forwards options to an API.

Python
def call_model(prompt, *stop_words, **options):
    print("prompt:", prompt)
    print("stop:", stop_words)
    print("options:", options)

call_model("Hi", "###", "END", temperature=0.3, max_tokens=50)
prompt: Hi stop: ('###', 'END') options: {'temperature': 0.3, 'max_tokens': 50}

Type hints

Definition · Type hint

An optional annotation that documents what type a parameter or return value should be, such as text: str or -> list[str]. Python does not enforce hints at runtime; editors and tools like mypy and pyright use them to catch bugs before you run the code.

Python
def chunk_words(text: str, size: int = 50) -> list[str]:
    """Split text into chunks of at most `size` words."""
    words = text.split()
    return [" ".join(words[i:i + size]) for i in range(0, len(words), size)]


def find_doc(doc_id: str, docs: dict[str, str]) -> str | None:
    """Return the document text, or None if the id is unknown."""
    return docs.get(doc_id)


print(chunk_words("one two three four five", size=2))
print(find_doc("a", {"a": "alpha"}), find_doc("z", {"a": "alpha"}))
['one two', 'three four', 'five'] alpha None

str | None means “a string or None”. Modern AI libraries such as Pydantic and FastAPI read type hints to validate data, so getting used to them now pays off quickly.

Watch out: mutable default arguments

Default values are created once, when the function is defined — not on every call. A list or dict default is therefore shared between calls:

Python
def add_message(text, history=[]):      # BUG: shared list
    history.append(text)
    return history

print(add_message("hi"))
print(add_message("again"))              # remembers the previous call!


def add_message_fixed(text, history=None):
    if history is None:
        history = []                     # fresh list on every call
    history.append(text)
    return history

print(add_message_fixed("hi"))
print(add_message_fixed("again"))
['hi'] ['hi', 'again'] ['hi'] ['again']

This bug is especially nasty in chatbots, where a shared default history leaks one user's conversation into another's. Always use None as the default for lists and dicts.

Scope: where variables live

Variables created inside a function are local: they exist only while the function runs and cannot be seen outside it. Functions can read module-level (global) variables, which is fine for constants like DEFAULT_MODEL, but avoid modifying globals — pass values in as parameters and return results instead.

Python
DEFAULT_MODEL = "gpt-4o-mini"      # module-level constant

def describe(model=None):
    chosen = model or DEFAULT_MODEL   # 'chosen' is local
    return f"using {chosen}"

print(describe())
print(describe("llama-3.1-8b"))
using gpt-4o-mini using llama-3.1-8b

Functions are values: lambda and sorting

In Python a function is an object like any other: you can store it in a variable or pass it to another function. A lambda is a tiny anonymous function written in one expression. The most common use is the key argument of sorted().

Python
results = [
    {"id": "a", "score": 0.62},
    {"id": "b", "score": 0.91},
    {"id": "c", "score": 0.78},
]

ranked = sorted(results, key=lambda r: r["score"], reverse=True)
print([r["id"] for r in ranked])
['b', 'c', 'a']

Errors and exceptions

Definition · Exception

An object Python raises when something goes wrong at runtime — a missing file (FileNotFoundError), a bad conversion (ValueError), a missing dictionary key (KeyError). If nothing handles it, the program stops and prints a traceback.

AI code talks to the outside world — files, networks, models that return malformed output — so handling exceptions is part of normal work. Use try/except to catch the specific errors you expect, and raise to signal invalid input yourself.

Python
def parse_score(raw: str) -> float:
    try:
        value = float(raw)
    except ValueError:
        print(f"could not parse {raw!r}, defaulting to 0.0")
        return 0.0
    if not 0.0 <= value <= 1.0:
        raise ValueError(f"score must be between 0 and 1, got {value}")
    return value

print(parse_score("0.83"))
print(parse_score("high"))

try:
    parse_score("7")
except ValueError as err:
    print("rejected:", err)
0.83 could not parse 'high', defaulting to 0.0 0.0 rejected: score must be between 0 and 1, got 7.0
  • Catch specific exceptions (except ValueError), never a bare except: — it hides real bugs and even swallows Ctrl+C.
  • An optional finally: block always runs, success or failure — use it for cleanup.
  • Read tracebacks from the bottom up: the last line names the exception, and the lines above show exactly where it happened.

Modules and imports

Definition · Module

Any .py file. The file text_utils.py is the module text_utils, and other files can import its functions. A folder of modules (traditionally containing an __init__.py) is a package.

As soon as a script grows beyond a page, split reusable helpers into modules. Python also ships with a large standard library of modules — json, pathlib, re, datetime, random, collections — that need no installation.

Import styleUsage afterwardsWhen to use
import jsonjson.dumps(...)Default — clear where names come from.
from pathlib import PathPath(...)A few frequently used names.
import numpy as npnp.array(...)Long names with a community-standard alias.
from x import *Everything, unqualifiedAvoid — it hides where names come from.

Your own module

text_utils.py
"""Reusable text helpers for AI pipelines."""

def normalize(text: str) -> str:
    return " ".join(text.lower().split())


def estimate_tokens(text: str) -> int:
    """Rough rule of thumb: about 4 characters per token for English."""
    return max(1, len(text) // 4)


if __name__ == "__main__":
    # Runs only with `python text_utils.py`, not on import.
    print(estimate_tokens("Retrieval augmented generation"))
main.py
from text_utils import normalize, estimate_tokens

doc = "  Agents   PLAN, act, and   observe.  "
clean = normalize(doc)
print(clean)
print("≈ tokens:", estimate_tokens(clean))
agents plan, act, and observe. ≈ tokens: 7
Definition · if __name__ == "__main__"

Every module has a built-in variable __name__. It equals "__main__" only when the file is run directly. Code under this check runs as a script but stays silent when the file is imported — perfect for quick demos and command-line entry points.

Note

Python finds modules in the folder of the script you run, then in installed packages. If from text_utils import normalize fails with ModuleNotFoundError, make sure you run python main.py from the folder that contains both files.

Watch out

Never name your own file after a library you import — openai.py, json.py, or random.py. Python will import your file instead of the real library and you will get baffling AttributeErrors.

Putting it together

Here is a small but realistic prompt-building helper that uses everything from this lesson: typed functions, defaults, strings, loops, conditions, and an exception for bad input.

Python
def format_context(chunks: list[str], max_chunks: int = 3) -> str:
    if not chunks:
        raise ValueError("need at least one context chunk")
    selected = chunks[:max_chunks]
    return "\n".join(f"[{i}] {c.strip()}" for i, c in enumerate(selected, start=1))


def rag_prompt(question: str, chunks: list[str]) -> str:
    return (
        "Answer the question using only the numbered context. "
        "Cite sources like [1].\n\n"
        f"{format_context(chunks)}\n\n"
        f"Question: {question.strip()}"
    )


print(rag_prompt(" What does RAG reduce? ", [
    "RAG grounds answers in retrieved documents.",
    "Grounding reduces hallucinations.",
]))
Answer the question using only the numbered context. Cite sources like [1]. [1] RAG grounds answers in retrieved documents. [2] Grounding reduces hallucinations. Question: What does RAG reduce?
Try it
  1. Write truncate(text: str, max_chars: int = 200) -> str that returns the text unchanged if it is short enough, otherwise cuts it and appends "…".
  2. Write label_score(score: float) -> str returning "high", "medium", or "low", and raise ValueError for scores outside 0–1.
  3. Move both functions into text_utils.py and call them from main.py.
Key takeaways
  • Python is dynamically but strongly typed: values carry their type, and conversions are explicit.
  • Use f-strings and string methods (strip, split, join, lower) for prompt and text work.
  • Empty values are falsy, so if not text: skips empty input; use is None for missing values.
  • Write small functions with clear names, docstrings, keyword arguments, and type hints.
  • Never use a list or dict as a default argument — use None and create it inside.
  • Catch specific exceptions, and split reusable code into modules guarded by if __name__ == "__main__".