Part 6

Make data flow explicit

The scope of a variable is the part of a program where that name is available. Function parameters and variables assigned inside a function are normally local: they belong to that function call. Names assigned outside functions are global to that module.

Local variables exist inside their function

Both parameters and other variables created in a function are local to it. They can be used while the function is running, but they do not become available elsewhere merely because the function was called:

def count_records(records):
    total = len(records)
    print(total)

count_records([{"title": "A"}, {"title": "B"}])
print(total)
Sample output

2 NameError: name 'total' is not defined

The name total exists only inside count_records. If another part of the program needs the value, the function should return it:

def count_records(records):
    total = len(records)
    return total

total = count_records([{"title": "A"}, {"title": "B"}])
print(total)

The two uses of the name total here are still separate variables: one local to count_records, and one assigned outside the function from its return value.

Local names can mask global names

A function can read a module-level variable if it has not created a local variable with the same name:

minimum_year = 1900

def included(year):
    return year >= minimum_year

print(included(1952))

This works, but the function has a hidden input: understanding it requires finding minimum_year elsewhere in the module.

Assignment changes how Python interprets a name. In the next example, minimum_year inside the function is a new local variable which masks the global one:

def choose_limit():
    minimum_year = 1950
    print(minimum_year)

minimum_year = 1900
choose_limit()
print(minimum_year)
Sample output

1950 1900

Python treats a name assigned anywhere in a function as local throughout that function unless told otherwise. Trying to read it before the local assignment therefore fails:

minimum_year = 1900

def choose_limit():
    print(minimum_year)
    minimum_year = 1950

choose_limit()
Sample output

UnboundLocalError: local variable 'minimum_year' referenced before assignment

The error does not mean the global value is missing. It means the later assignment caused Python to interpret this particular minimum_year as local before it had a value.

What global changes

The global keyword declares that assignments in a function should target a module-level variable instead of creating a local one:

processed_records = 0

def record_processed():
    global processed_records
    processed_records += 1

record_processed()
print(processed_records)
Sample output

1

Without global, the assignment would target a local name. The keyword is important to recognise when reading code, but changing shared state this way makes inputs and outputs harder to follow. Most pipeline values should travel through parameters and return values instead.

The following function uses that explicit pattern. Its source records and selected language arrive as parameters, and its result leaves through return:

def count_language(records, language):
    count = 0
    for record in records:
        if record["language"] == language:
            count += 1
    return count

records = [
    {"title": "A", "language": "Finnish"},
    {"title": "B", "language": "Swedish"},
]

print(count_language(records, "Finnish"))

The names records, language and count inside count_language are local. The separate records in the final part of the program is global. Passing it as an argument makes the connection visible.

A maintenance warning: hidden input

Generated code sometimes depends on a global value without making that dependency clear:

selected_language = "Finnish"

def matching_records(records):
    matches = []
    for record in records:
        if record["language"] == selected_language:
            matches.append(record)
    return matches

To understand this function, you have to search elsewhere for selected_language. A caller also cannot select a different language without first changing shared state. Make the input explicit instead:

def matching_records(records, selected_language):
    matches = []
    for record in records:
        if record["language"] == selected_language:
            matches.append(record)
    return matches

This small repair matters in analysis pipelines. A dashboard filter, a code cell in a notebook or a test can now pass its selected value directly, and the returned records can be inspected before presentation.

When reviewing unfamiliar code, ask:

  1. Which values enter this function as parameters?
  2. Which values are created locally?
  3. Which values are read from outside the function?
  4. Does the function return the transformed data, or silently change shared state?

The global keyword can make assignment target a module-level name, but it is rarely the right way to move analytical data between pipeline stages. Prefer parameters for inputs and return values for outputs. Module-level constants, such as a fixed column-name list, are reasonable when they truly are configuration rather than changing data.

Main functions keep orchestration local

An explicit main function can connect the stages without making intermediate values global:

def main():
    records = read_records("collections.csv")
    selected = matching_records(records, "Finnish")
    rows = summarize(selected)
    print_rows(rows)

if __name__ == "__main__":
    main()

Here records, selected and rows are local to main. Each pipeline function receives the data it needs and returns the value used by the next stage. This is the shape to look for when an agent proposes a multi-function program.

You have reached the end of this section! Continue to the next section:

You can check your current points from the blue blob in the bottom-right corner of the page.