Programming Foundations

Values, Types, Control Flow, Functions, and Modules

Connect values and types with branches, loops, functions, scope, and mutability through small Python examples.

About 60 min · Updated 2026-10-25

Information checked as of: 2026-10-26

Learning objectives

  • Trace values, types, expressions, branches, and loops to explain changes in program state.
  • Divide responsibilities using functions, scope, and modules, and write code that controls mutable state.

You call a function and discover that the list you passed into it has changed. Understanding why requires looking at assignment, references, arguments, and mutability together. Using Python examples, this unit traces short programs in execution order and explains where values go and which objects change.

Distinguish values, types, and expressions

Values are the data a program works with; a type determines which operations are supported by a value. In Python, 3 is an integer and "3" is a string. Similar-looking values can behave differently under addition or comparison. An expression is evaluated to produce a value: 3+4 produces 7. An assignment statement, by contrast, binds a name to an object so that later calculations can use it.

price = 120
count = 3
total = price * count
print(total)             # 360
print("120" * count)     # 120120120

Dynamic typing does not mean “there are no types.” Python objects have types, and the values present at runtime determine whether an operation is valid. Type annotations communicate intent to readers and checking tools. Writing an annotation alone does not automatically validate every value at runtime. Make conversions and checks explicit at input boundaries.

Control flow determines execution order

Sequence, selection, and iteration combine operations according to the input. An if statement selects a branch according to a condition; a for loop visits items in turn. To understand a loop, separate the initial state, the state changed by each iteration, and the termination condition. Check that your explanation still holds for empty inputs and boundary values, rather than only typical examples.

def sum_positive(values):
    total = 0
    for value in values:
        if value > 0:
            total += value
    return total

print(sum_positive([-2, 0, 3, 5]))  # 8
print(sum_positive([]))             # 0

This loop maintains the property “total contains the sum of the positive values seen so far.” Treating that property as an invariant explains why 0 is the correct initial value, why negative numbers are not added, and why the answer is available when the loop finishes. A return statement ends the function immediately, so changing its position changes which inputs are processed.

Read functions through inputs, return values, and side effects

A function can compute a return value from arguments, change external state, or do both. Writing to a file and mutating a list are side effects. Two functions with the same return value may require different handling if one changes the caller’s data. Names and documentation are more useful when they explain both what is returned and whether arguments are mutated.

def add_item(items, value):
    items.append(value)

a = [1]
b = a
add_item(a, 2)
print(a, b)              # [1, 2] [1, 2]

b = [9]                 # rebind b to a different list
print(a, b)              # [1, 2] [9]

Initially, a and b refer to the same list. append mutates that list, so the change is visible through both names. The assignment b=[9] changes what b refers to; it does not mutate the list referenced by a. Distinguish “the variables have equal values” from “the variables refer to the same object.”

Scope and modules

Scope determines where a name can be referenced. A name assigned inside a function is normally local to that function. If a name also exists outside it, check which binding is being used. Functions that silently change widely shared state are hard to trace. Passing required values as arguments and returning results usually makes dependencies easier to follow.

A module groups related definitions and provides a namespace. Pay attention to what happens merely by importing it. Expensive computations or file changes at module level can cause side effects during import. Separating reusable function definitions from code intended to run as a command makes testing and reuse easier.

Check your understanding

  • What happens if return in sum_positive is moved inside the for loop? The function exits after processing the first element. With an empty input, the loop does not run, and Python returns None if execution reaches the end of the function.

  • After a=[1] and b=a, what happens to a when b.append(2) runs? It becomes [1, 2], because both names refer to the same list. With b=a.copy(), the outer lists would be separate objects.

  • Must a function reject an argument of the wrong type simply because it has annotations? No. Python annotations alone do not provide that guarantee. Implement required validation separately.

Concepts in this unit: Type · Scope · Mutability ·

Read next: Memory, Pointers, References, and Lifetimes