v9.1.1
Menu

Chapter 1: Python Foundations

Essential Python programming for AI and ML: variables, data structures, control flow, functions, classes, data manipulation with NumPy/Pandas, and introduction to scikit-learn.

Topics
9
Videos
9
Min read
80

Chapter Overview

Python is the dominant language for AI, machine learning, and computer vision. Its readable syntax, vast ecosystem of libraries, and active community make it the default choice for practitioners worldwide -- from academic research to production systems.

Before diving into algorithms, you need a solid foundation in Python's core features. You'll work with numerical data stored in arrays, text labels for classification, and dictionaries for configuration. You'll write functions to preprocess data and classes to build reusable components. Understanding these fundamentals makes the difference between struggling with syntax and focusing on the concepts that matter.

Why Python? Python offers:

  • Rich ecosystem: NumPy, Pandas, OpenCV, PyTorch, TensorFlow, scikit-learn all have Python APIs
  • Readability: Code is easy to write and understand
  • Rapid prototyping: Quick iteration on ideas
  • Large community: Extensive resources and support

What you'll learn in this chapter:

  1. Variables and Data Types -- How Python handles numbers, strings, booleans, and type annotations
  2. Lists and Collections -- Storing and manipulating sequences of data
  3. Dictionaries -- Key-value storage for structured data
  4. Control Flow -- Loops and conditionals for algorithm logic
  5. Functions -- Reusable code blocks, decorators, and functional patterns
  6. Classes -- Object-oriented design and the fit/predict pattern
  7. NumPy/Pandas Basics -- Fast numerical arrays and tabular data manipulation
  8. Data Loading -- Getting data from files into Python efficiently
  9. Scikit-learn Intro -- The consistent estimator API that powers most ML projects

Each topic builds toward writing production-quality code. Lists become feature arrays, dictionaries store model configs, loops iterate over batches, classes wrap models and datasets, and NumPy/Pandas power all numerical computation.

Chapter Roadmap

Click any topic to jump in

Explainer video
1
Variables & Data Types

Python's type system, mutability, and memory model — the building blocks everything else depends on.

Variables as LabelsNumeric TypesStringsBooleans and NoneType AnnotationsMutable vs Immutable
Core data structures

Sequences and mappings

2
Lists & Collections

Ordered sequences, slicing, and comprehensions — the core data structures for processing datasets.

Lists: Ordered Mutable SequencesIndexing and SlicingList ComprehensionsTuples and Sets
3
Dictionaries

Hash-based key-value storage — O(1) lookups that power feature maps, configs, and data pipelines.

Dictionary BasicsDictionary MethodsDictionary ComprehensionsNested Dictionaries
Processing and composing logic

Iteration and abstraction

4
Control Flow

Conditionals, loops, iteration patterns — controlling program execution for data processing and training loops.

Conditional StatementsFor LoopsWhile Loops and ControlRange and Enumerate
5
Functions

Defining reusable logic with args, kwargs, lambdas — composing operations for clean ML pipelines.

Defining FunctionsArguments and Defaults*args and **kwargsLambda FunctionsPure Functions
Building reusable ML components
6
Classes & Objects

Object-oriented patterns, inheritance, and special methods — the foundation of PyTorch modules and sklearn estimators.

Class DefinitionInstance vs Class AttributesSpecial MethodsInheritanceThe __init__ and fit Pattern
The ML ecosystem

NumPy, Pandas, and scikit-learn

7
NumPy/Pandas Basics

Vectorized computation and DataFrames — replacing Python loops with fast array operations.

VectorizationBroadcasting
8
Data Loading

Reading datasets efficiently with generators and memory mapping — handling data larger than RAM.

Data Loading Best PracticesMemory Efficiency
9
Scikit-learn Intro

The Estimator API, transformers, and pipelines — the standard interface for ML in Python.

Estimator APITransformersPipelines

Topics

9 topics in this chapter

You load a dataset, make a "copy" to experiment on, overwrite one value, and later discover that the original data changed too. Or two numbers that should be equal, 0.1 + 0.2 and 0.3, compare as unequal and a unit test fails. Neither bug produces an error message. Both come from not knowing exactly what a Python variable is and what type of value it refers to. Before you can write a training loop or a data pipeline, you need a precise picture of how names, objects, and types behave.

The previous chapter, Development Setup, gave you a working Python installation, an editor, and a virtual environment. This topic starts using them. Every later topic in this chapter, from lists to scikit-learn pipelines, is built out of the values introduced here.

We begin with variables as labels that point at objects, then cover the numeric types and their precision limits, strings, booleans and None, and type annotations that document intent. We finish with mutability, which explains the vanishing copy above and returns in every chapter that follows.

Definition

A Python variable is a name bound to an object. The object, not the name, carries a type such as int, float, str, bool or NoneType, and an identity reported by id(). Assignment binds a name without copying anything. An object is mutable if its contents can change in place, as with lists, dicts and NumPy arrays, and immutable otherwise, as with numbers, strings and tuples.

In this topic

  1. 1Variables as Labels
  2. 2Numeric Types
  3. 3Strings
  4. 4Booleans and None
  5. 5Type Annotations
  6. 6Mutable vs Immutable
1 of 6
Variables as Labels

variable→value in memory\text{variable} \rightarrow \text{value in memory}

Think of a variable as a sticky note with a name on it. You stick it on an object (a value in memory), and Python remembers where you put it. The key insight: the sticky note can be moved to a different object at any time.

x = 42       # x points to integer 42
x = "hello"  # x now points to a string instead

Why this matters: When you write data = load_data(), you're not putting data "inside" a variable -- you're creating a label that points to data in memory. This explains why copy_data = data doesn't create a copy -- both labels point to the same data!

Mathematical Intuition

Python variables are pointers stored on the stack, each holding a reference (memory address) to a heap-allocated object. Assignment a = b copies the 8-byte pointer, not the object — O(1)O(1) regardless of object size. The id() function returns this address. Reference counting tracks how many pointers target each object; when the count hits zero, the object is freed. Circular references require the cyclic garbage collector, which runs in O(n)O(n) where nn is the number of objects in the cycle.

Example:

You load a dataset and modify a 'copy'. Original also changes! Why? data = load_data(); copy = data; copy[0] = 99

2 of 6
Numeric Types

int,float,complex\texttt{int}, \texttt{float}, \texttt{complex}

Integers (int) are exact whole numbers with unlimited size -- Python handles arbitrarily large numbers automatically:

big = 10 ** 100  # 1 followed by 100 zeros!

Floats (float) are approximations using 64-bit "floating point" representation. They're fast but have tiny precision errors:

0.1 + 0.2  # 0.30000000000000004, not exactly 0.3

Complex numbers store both real and imaginary parts -- essential for Fourier transforms in signal processing and image analysis:

z = 3 + 4j  # Represents point (3, 4) in the complex plane
abs(z)      # 5.0 - the distance from origin
Mathematical Intuition

Python int uses arbitrary-precision arithmetic: internally it stores an array of 30-bit "digits," so a kk-bit integer uses ⌈k/30⌉\lceil k/30 \rceil machine words. Addition is O(k)O(k), multiplication uses Karatsuba's algorithm at O(klog⁡23)≈O(k1.585)O(k^{\log_2 3}) \approx O(k^{1.585}). Floats are IEEE 754 double-precision: 1 sign bit, 11 exponent bits, 52 mantissa bits, giving ≈15\approx 15–16 significant decimal digits and a range up to ∼1.8×10308\sim 1.8 \times 10^{308}. The classic 0.1+0.2≠0.30.1 + 0.2 \neq 0.3 arises because 0.10.1 has no exact binary fraction representation.

Example:

A feature value 128 needs to be normalized to [0,1]. You write: normalized = 128 / 255. What type is the result?

3 of 6
Strings

str=immutable sequence of characters\texttt{str} = \text{immutable sequence of characters}

Strings are immutable -- once created, they cannot be changed. This might seem limiting, but it makes strings safe to share and use as dictionary keys.

Why immutable? Imagine if you could change a filename string after using it as a dictionary key -- chaos! Instead, operations like replace() create entirely new strings.

path = "/data/train/sample_001.csv"
name = path.split("/")[-1]  # "sample_001.csv"
base = name.replace(".csv", "")  # Creates new string "sample_001"

f-strings embed expressions directly -- cleaner than concatenation:

f"Processing {name} ({n_samples} samples)"
Mathematical Intuition

Python strings are immutable arrays of Unicode code points stored as compact arrays (Latin-1, UCS-2, or UCS-4 depending on the widest character). Indexing is O(1)O(1) since each character occupies a fixed number of bytes within its encoding. Concatenating nn strings with + in a loop is O(n2)O(n^2) total because each concatenation copies all previous characters into a new allocation. Use ''.join(parts) for O(n)O(n) concatenation. String hashing uses SipHash, computed lazily and cached — so using a string as a dict key is O(L)O(L) on first use but O(1)O(1) thereafter, where LL is the string length.

Example:

You have path '/data/train/sample_001.csv'. Extract: folder name, file name without extension, and the number.

4 of 6
Booleans and None

bool∈{True,False}\texttt{bool} \in \{\texttt{True}, \texttt{False}\}

Booleans represent yes/no, true/false decisions. They're the foundation of all conditional logic:

is_valid = n_samples > 0 and n_features > 0
has_labels = target is not None

None is Python's way of saying "nothing here" -- it's not zero, not an empty string, but the explicit absence of a value:

result = None  # "I don't have a result yet"
if result is None:
    result = compute()  # Now I do

Truthiness is Python's way of treating non-boolean values as true/false:

  • Falsy (treated as False): 0, 0.0, "", [], {}, None
  • Truthy (treated as True): Everything else

This lets you write if data: instead of if len(data) > 0:

Mathematical Intuition

bool is a subclass of int in Python: True == 1 and False == 0, so booleans participate directly in arithmetic. None is a singleton — there is exactly one None object in the interpreter, which is why is None (pointer comparison, O(1)O(1)) is preferred over == None (which invokes __eq__, potentially O(n)O(n) for custom types). Truthiness evaluation calls __bool__() first, then __len__() as a fallback; NumPy arrays deliberately raise ValueError on __bool__ when they contain more than one element to prevent ambiguous truth testing.

Example:

Check if data loaded successfully: data = load_array(path). Why is if data: dangerous? What's correct?

5 of 6
Type Annotations

def train(X: np.ndarray, y: np.ndarray) -> Model\text{def train(X: np.ndarray, y: np.ndarray) -> Model}

Python does not check types before running, so passing a list where a function expects a NumPy array fails only when the offending line executes, sometimes hours into a training job. Type annotations, standardized by PEP 484, write the expected types into the signature. In the formula, X: np.ndarray declares a parameter type and -> Model the return type. The interpreter stores annotations in __annotations__ but never enforces them. Static checkers such as mypy, and Pylance from Development Setup, read them and flag mismatches as you type. Generic forms such as list[int] and tuple[np.ndarray, np.ndarray] describe containers, and Optional[np.ndarray] marks a value that may be None, from Booleans and None.

Mathematical Intuition

Type annotations are stored in __annotations__ dictionaries at function and module level but have zero runtime cost — CPython ignores them during execution. Static analyzers like mypy perform Hindley-Milner-style type inference extended with gradual typing: annotated code is checked at O(n)O(n) in the size of the AST, while unannotated functions are treated as Any. Generic types like list[int] use parametric polymorphism, and TypeVar enables bounded quantification: T = TypeVar('T', bound=Comparable) restricts TT to types with ordering.

Example:

Write a function signature with type hints: function predict takes a numpy array X and returns a numpy array of predictions.

6 of 6
Mutable vs Immutable

Lists are mutable (can change), while tuples and strings are immutable. NumPy arrays are mutable but have fixed size after creation. Understanding mutability is critical for avoiding subtle bugs when copying data.

# Mutable: changes affect all references
a = [1, 2, 3]
b = a       # b points to same list
b[0] = 99   # a is now [99, 2, 3] too!

# Immutable: safe from accidental changes
t = (1, 2, 3)
# t[0] = 99  # TypeError!
Mathematical Intuition

Immutable objects can be safely shared across references because no mutation is possible — this is why Python interns small integers (−5-5 to 256256) and short strings, reusing the same object for identical values. Mutable objects like lists use a dynamic array internally: append() is amortized O(1)O(1) because the array doubles capacity when full (1,4,8,16,25,35,...1, 4, 8, 16, 25, 35, ...), giving a geometric growth pattern. Copying a list with a.copy() is O(n)O(n) — it copies the pointer array but not the underlying elements (shallow copy). copy.deepcopy() recursively copies all nested objects at O(N)O(N) where NN is the total object graph size.

Example:

What happens? a = [1, 2, 3]; b = a; b[0] = 99; print(a)

Theory Exercise

Problem:

What will be printed? Explain each step.

a = 10
b = a
a = 20
print(b)
Hints:
  • Variables are references to objects, not containers
  • Assignment creates a new reference
  • Integers are immutable in Python

Coding Exercise

Problem:

Create a dictionary that maps model names to their hyperparameter dictionaries.

Hints:
  • Use nested dictionaries
  • Include at least 2 models