AI ENGINEER00-01 · Python foundations for reliable AI applications
20/90
Sign in

Layer 00 · Foundations · Ch 01

Python foundations for reliable AI applications

Build dependable AI application code with clear data, streaming, cleanup, errors, logs, and project structure.

70 min readdifficulty beginnerdepth build

Python is the glue in many AI applications.

The AI model may produce the answer. Python handles the work around it. Python receives the user’s text, cleans it, sends it to the model, checks the reply, and decides what happens next.

You do not need to know all of Python before you can build something useful. You need a small set of ideas and a clear picture of how data moves through them.

Lesson details

Module
AI engineering foundations
Lesson
1 · Python foundations for AI applications
Difficulty
Beginner
Learning time
32 minutes

Before you start

  • You can use a web browser
  • You have completed How to use this course
  • No previous Python is required

Skills you will gain

  • Store text in variables and lists
  • Read a small Python function
  • Clean a group of document strings
  • Recognise and fix a shared-list default bug
  • Use type hints and dataclasses to describe AI application data
  • Choose a comprehension or generator for data processing and streaming
  • Add shared behaviour with decorators and cleanup with context managers
  • Handle expected errors, log safely, and organise a small AI project

Why this matters

AI applications receive untidy data. A support message may contain extra spaces. A document list may contain blank entries. A model should not have to guess which text is useful.

Python lets you turn that messy input into a predictable shape before another part of the system uses it.

Start with four small building blocks

A variableVariableA name that points to a value in a program. In `message = "hello"`, the name `message` lets you use that text again later. is a name that points to a value.

message = "  Where is my order?  "

The name is message. The value is the text inside the quotation marks.

A listListAn ordered Python collection that can hold several values. Lists use square brackets, such as `["one", "two"]`. keeps several values in order. Python lists use square brackets:

messages = ["  Refund please  ", "   ", "Where is my order?"]

A loop repeats an instruction. A for loop can visit each item in a list, one at a time.

A functionFunctionA named, reusable set of instructions. A function can receive input through parameters and return a result. is a named, reusable group of instructions. A function can receive an input and return an output.

See the whole idea

FIG 00-01.1 · MESSY TEXT BECOMES USEFUL INPUT
A small Python document-cleaning pipelineThree raw document strings enter a clean function. The function strips surrounding spaces, ignores empty text, and returns two clean document strings for an AI model.RAW DOCUMENTS" Refund please "" ""Where is my order?"clean()strip + keepCLEAN TEXT"Refund please""Where is..."input → small dependable steps → useful output
FIG 00-01.1— three raw strings enter the clean function. Surrounding spaces and one blank string are removed, leaving two useful strings.

The diagram contains three stages:

  1. Input: a list holds three pieces of text.
  2. Process: the function removes surrounding spaces and ignores text that becomes empty.
  3. Output: a new list holds only useful text.

The original input stays separate from the result. This makes the function easier to reason about.

Follow the steps

Step 1: receive the input

The function receives a list through a parameter. A parameter is the name a function uses for an input.

def clean_messages(messages):

Here, messages is the parameter.

Step 2: make an empty output list

cleaned = []

The function needs somewhere to collect its results. It starts with an empty list.

Step 3: visit each message

for message in messages:

On each pass through the loop, message points to one value from the input list.

Step 4: clean and check the text

tidy = message.strip()
if tidy:

The string method .strip() removes spaces from the beginning and end.

An if statement runs its indented code only when a condition is true. An empty string counts as false, so if tidy: rejects blank text.

Step 5: return the result

cleaned.append(tidy)
return cleaned

.append() adds one value to a list. return sends the finished list back to the code that called the function.

Worked example

Trace this input by hand:

["  Refund please  ", "   ", "Where is my order?"]
Loop pass Current value After .strip() Keep it? Output so far
1 " Refund please " "Refund please" Yes ["Refund please"]
2 " " "" No ["Refund please"]
3 "Where is my order?" unchanged Yes ["Refund please", "Where is my order?"]

Nothing mysterious happens between input and output. The function applies the same two checks to every item.

Run the code

PythonClean a list of support messages
def clean_messages(messages):
    cleaned = []

    for message in messages:
        tidy = message.strip()
        if tidy:
            cleaned.append(tidy)

    return cleaned


raw_messages = ["  Refund please  ", "   ", "Where is my order?"]
print(clean_messages(raw_messages))

Important lines

def clean_messages(messages):
Creates a reusable function and names its input messages.
cleaned = []
Creates a fresh output list each time the function runs.
for message in messages:
Visits every input string, one at a time.
tidy = message.strip()
Removes spaces from both ends without changing spaces inside the sentence.
if tidy:
Runs the next line only when some text remains.
return cleaned
Sends the completed list back to the caller.

Now run it in your browser. Change one message. Add an empty string. Predict the output before selecting Run.

LAB 00-01.A · CLEAN THE SUPPORT QUEUE
IDLE⌘↵ to run

A bug worth remembering

The original lesson opened with a Python trap that has appeared in real software:

def add_message(message, batch=[]):
    batch.append(message)
    return batch

print(add_message("first"))
print(add_message("second"))

The surprising output is:

['first']
['first', 'second']

Python creates the default list once, when it defines the function. Later calls reuse that same list.

Use None as the default, then create a new list inside:

def add_message(message, batch=None):
    if batch is None:
        batch = []
    batch.append(message)
    return batch

This matters in an AI system because shared state can mix data from separate requests.

Part 2 — Python patterns for reliable AI applications

The first part gives you the language basics. These patterns make a small AI application easier to change, test, and debug.

FIG 00-01.2 · THE PYTHON AROUND A MODELreliable Python around the modeldescribe datatype hints → expected shapedataclass → related fieldsvalidate at boundariesmove datacomprehension → short map/filtergenerator → one at a timestream large work safelywrap behaviourdecorator → retry / timingwith → always clean upkeep core logic focusedmake failures usefulspecific exceptionssafe structured logstests + clear layoutmodel code is one part; reliability lives in the Python around it
FIG 00-01.2— dependable AI systems need clear data shapes, careful streaming, shared reliability behaviour, cleanup, visible failures, and a predictable project layout. The model call is only one step.

Type hints say what data is expected

Type hintsType hintsOptional annotations that declare the expected types of variables, arguments and return values (e.g. `def f(x: int) -> str`). Python does not enforce them at runtime, but tools like mypy, your editor, and libraries like Pydantic read them to catch bugs before the code ever runs. are labels for expected data shapes.

def clean_documents(documents: list[str]) -> list[str]:
    return [document.strip() for document in documents if document.strip()]

The function expects a list of strings and returns a list of strings. Python does not enforce this at runtime, but editors, type checkers, and validation libraries can spot mismatches early. Hints do not replace validation for untrusted user or API data.

A dataclassDataclassA class defined with the `@dataclass` decorator, which auto-generates the boilerplate — `__init__`, `__repr__`, equality — from typed field declarations. The idiomatic way to model a bundle of configuration or structured data without writing constructors by hand. is a lightweight way to name related configuration values.

PythonKeep chunking configuration together
from dataclasses import dataclass, field

@dataclass
class ChunkConfig:
    size: int = 300
    overlap: int = 40
    sources: list[str] = field(default_factory=list)

config = ChunkConfig(sources=["handbook.md"])
print(config)

Important lines

@dataclass
Creates useful initialisation and display behaviour from declared fields.
size: int = 300
Names an integer field and its default value.
field(default_factory=list)
Creates a fresh list for every object, avoiding the shared mutable-default bug.

Comprehensions are short, visible transformations

A comprehensionComprehensionA compact Python expression that constructs a list, set, or dictionary by mapping and optionally filtering items. It is best for one clear transformation, not a long hidden workflow. builds a collection in one clear expression.

titles = [document.title() for document in documents if document.strip()]

Read it left to right: produce a title for each non-blank document. Use a comprehension for one obvious map or filter. Use a normal loop when steps need names, error handling, logging, or more than one decision.

Generators stream one value at a time

A generatorGeneratorA function that uses `yield` to produce values one at a time, on demand, instead of building a whole list in memory. The consumer pulls the next value when it wants it; the generator pauses in between. It is how you stream LLM tokens as they arrive and how you process datasets too large to fit in memory. uses yield to pause and produce each value only when the next consumer asks for it.

def non_blank_lines(path):
    with open(path, encoding="utf-8") as file:
        for line in file:
            tidy = line.strip()
            if tidy:
                yield tidy

Lists are eager: they build every value now. Generators are lazy: they make the next value when needed. This suits large files, document pipelines, and streaming model tokens. Generators are single-use; make a new generator to iterate again.

Decorators add shared behaviour around a function

A decoratorDecoratorA function that wraps another function to add behaviour around it — retries, timing, caching, logging — without changing the wrapped function's own code. Applied with the `@decorator` syntax on the line above a `def`. The standard way to attach cross-cutting concerns to the many API calls in an AI system. wraps a function to add timing, retries, rate limiting, or logging.

from functools import wraps
from time import perf_counter

def timed(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        result = function(*args, **kwargs)
        print("seconds:", round(perf_counter() - start, 3))
        return result
    return wrapper

Keep decorators small. Too many stacked wrappers hide control flow and make debugging difficult. Retry only errors you expect and can safely repeat.

Context managers guarantee cleanup

A context managerContext managerAn object used with a `with` block that guarantees setup and teardown run around a piece of code — even if it raises. `__enter__` runs on the way in, `__exit__` on the way out, always. Used to open and reliably close files, network clients, database connections and timers. is used with with. It guarantees cleanup even if code inside fails.

with open("ticket.txt", encoding="utf-8") as file:
    ticket = file.read()
# file is closed here, even if read() raised an exception

Use context managers for files, database transactions, network clients, locks, and timing scopes.

Handle expected errors and log without secrets

Exception handlingException handlingCatching a specific expected runtime error, recovering or adding useful context, and allowing unknown or important errors to remain visible. It prevents a known failure from crashing an entire workflow without hiding bugs. catches a specific expected error. Do not use a bare except, which catches everything and can hide programming mistakes.

import logging

logger = logging.getLogger(__name__)

def parse_limit(raw_limit: str) -> int:
    try:
        limit = int(raw_limit)
    except ValueError as error:
        raise ValueError("limit must be a whole number") from error

    logger.info("parsed document limit: %d", limit)
    return limit

LoggingLoggingRecording structured operational events such as request counts, warnings, errors, and timing so a system can be observed and debugged. Logs must not contain secrets, API keys, or unnecessary personal data. records operational events. Prefer it to print in application code. Use placeholders so expensive values are not formatted unless needed. Never log API keys, passwords, full private prompts, or personal data unless a narrowly justified policy permits it.

Give the project a home for each concern

A project layoutProject layoutA deliberate organisation of source code, tests, configuration, and documentation so an application is easy to navigate, import, test, and deploy consistently. prevents one giant file from becoming the whole application.

support_assistant/
├── pyproject.toml
├── README.md
├── src/support_assistant/
│   ├── config.py
│   ├── clients.py
│   ├── pipeline.py
│   └── logging.py
└── tests/

Keep configuration, external clients, domain pipeline logic, and tests separate. A src layout also makes accidental imports from the working directory less likely.

Real-world AI engineering use

The cleaning function is not artificial intelligence. It is software engineering around the model. AI engineering needs both.

Common mistakes

Changing the input list while looping over it.

Fix — Build a separate output list. Removing items from the list you are visiting can make Python skip the next item.

Forgetting to return the result.

Fix — Add return cleaned after the loop. Without return, Python gives the caller None.

Putting return inside the loop.

Fix — Align return with the for line. If it is inside the loop, the function stops after the first message.

Using an empty list as a default parameter.

Fix — Use None and create a fresh list inside the function so separate calls do not share data.

Knowledge check

Choose an answer. Feedback appears immediately and explains why.

Knowledge check

1What is a variable?
2What does message.strip() change?
3Why is return placed after the loop?
4What is wrong with def add_message(message, batch=[])?
5What is a type hint useful for?
6When is a generator especially useful?
7What does a context manager help guarantee?
8Why avoid a bare except clause?

Practice activity

Practice · 10–20 minutes

Prepare feedback for a sentiment model

Goal: Write a function that cleans feedback strings and keeps only useful entries.

  1. Start with the code below.
  2. Create an empty results list.
  3. Loop over every feedback string.
  4. Remove surrounding spaces.
  5. Keep the text only when it is not empty.
  6. Return the finished list and print it.

Expected result: ["Great service", "Delivery was late"]

Optional challenge: Return each kept message in lowercase by calling .lower() after .strip().

Reveal solution

Starter code:

def prepare_feedback(items):
    # Write your code here
    pass


feedback = ["  Great service ", "", "   ", "Delivery was late  "]
print(prepare_feedback(feedback))
def prepare_feedback(items):
    prepared = []

    for item in items:
        tidy = item.strip()
        if tidy:
            prepared.append(tidy)

    return prepared


feedback = ["  Great service ", "", "   ", "Delivery was late  "]
print(prepare_feedback(feedback))
# ['Great service', 'Delivery was late']

For the optional challenge, change tidy = item.strip() to:

tidy = item.strip().lower()

Key takeaways

  • A variable gives a value a useful name.
  • A list keeps several values in order.
  • A loop repeats the same operation for each item.
  • A function turns named steps into reusable input-to-output behaviour.
  • Create fresh output lists so separate calls do not accidentally share data.

Flashcards

Flashcards

1 of 12

Revision and interview questions

  1. Explain the path from raw_messages to the returned cleaned list.
  2. What would happen if return cleaned were indented inside the for loop?
  3. Why should an AI application clean and validate text before sending it to a model?
  4. In an interview, how would you explain the mutable-default list bug and its fix?
  5. Compare a comprehension with a generator and choose one for a large document stream.
  6. Explain how a decorator and context manager keep AI application code reliable.
  7. Describe a safe exception-and-logging policy for an API response parser.

One-page sketchnote summary

Print me · one-page revision sheet

Python foundations for AI applications

Main idea

Python connects the parts of an AI application. A dependable function receives messy input, applies small visible steps, and returns predictable output.

Core terms

  • Variable — a name for a value
  • List — ordered values in square brackets
  • Loop — repeats work
  • Function — reusable named instructions
  • Return — sends a result back
  • Type hint — expected data shape
  • Generator — on-demand values
  • Context manager — guaranteed cleanup

One picture

A small Python document-cleaning pipelineThree raw document strings enter a clean function. The function strips surrounding spaces, ignores empty text, and returns two clean document strings for an AI model.RAW DOCUMENTS" Refund please "" ""Where is my order?"clean()strip + keepCLEAN TEXT"Refund please""Where is..."

Code pattern

result = []
for item in items:
  tidy = item.strip()
  if tidy:
      result.append(tidy)
return result

Watch out

  • Do not return from the first loop pass
  • Do not mutate the list you are visiting
  • Do not use [] as a default parameter
  • Do not use bare except
  • Do not log secrets or personal data

Remember

  • Name data clearly
  • Move data through small steps
  • Build a fresh result
  • Check output before using a model
  • Stream large work when possible
  • Keep shared reliability behaviour small

Three quick questions

  1. What does .strip() remove?
  2. Why is return after the loop?
  3. How do you make a safe list default?
  4. Why is a generator single-use?
  5. What does with guarantee?

Lesson checklist

0 of 11 complete

Resources

Next up
Async Python
You can now follow data through a small function. Next you will learn how Python can pause while an AI model or another service is working, so one slow request does not make every other task wait.