---
title: Classes and Functions in Python (Pure Functions vs. Modifiers)
source: https://app.sythra.ai/learn/python/classes-and-functions-python
topic: Python
updated: 2026-08-12
publisher: Sythra (https://app.sythra.ai)
---

# Classes and Functions in Python (Pure Functions vs. Modifiers)

A pure function returns a new object without touching its inputs, while a modifier changes the object it receives directly. Planned development — reframing a Time object as a base-60 number — replaces messy overflow-checking code with a few clean lines using divmod().

_Source: [https://app.sythra.ai/learn/python/classes-and-functions-python](https://app.sythra.ai/learn/python/classes-and-functions-python) — free to read on Sythra._

## Key points

- A pure function builds and returns a new object, leaving its inputs untouched
- A modifier changes the object passed into it and typically returns nothing
- Prototype and patch (write, test, fix) tends to produce fragile, special-case code
- Planned development — reframing Time as seconds since midnight — makes overflow handling trivial with divmod()
- assert checks an object's invariants and crashes immediately if violated, making hidden assumptions visible

In [Classes and Objects](/learn/python/classes-and-objects-python), you built objects — you created a `Point`, gave it an `x` and a `y`, and passed it around. That was the first step: knowing how to hold data inside a custom type.

This article asks a bigger question: what do you _do_ with those objects? How do you write functions that work with them — and write those functions _well_? To explore this, we introduce a new class called `Time`, which stores a time of day, and use it to teach two big ideas: **pure functions vs. modifiers**, and **prototype and patch vs. planned development**. Along the way you'll meet **invariants** — the idea of keeping your objects honest.

## What you will learn

- How to build functions that read and combine custom objects
- The difference between a pure function and a modifier
- Why "prototype and patch" tends to produce fragile, special-case code
- How reframing a problem (planned development) can make it dramatically simpler
- How to protect an object's invariants with `assert`

## The Time class

Imagine a cinema app that needs to track when a movie starts, how long it runs, and when it ends. For that you need a way to represent a time of day:

```python
class Time:
    """Represents the time of day.

    attributes: hour, minute, second
    """

time = Time()
time.hour = 11
time.minute = 59
time.second = 30
```

Nothing new here — same pattern as `Point`. This `Time` object represents 11:59:30, thirty seconds before noon. Here's a function to print it nicely, using the format specifier `%02d` to pad single digits with a leading zero:

```python
def print_time(t):
    print(f'{t.hour:02d}:{t.minute:02d}:{t.second:02d}')

print_time(time)    # 11:59:30
```

And a function to compare two times without writing a single `if` — Python compares tuples element by element, hour first, then minute, then second:

```python
def is_after(t1, t2):
    return (t1.hour, t1.minute, t1.second) > (t2.hour, t2.minute, t2.second)
```

## Pure functions

Suppose you want to add two `Time` objects together — start time plus duration equals end time:

```python
def add_time(t1, t2):
    total = Time()
    total.hour   = t1.hour   + t2.hour
    total.minute = t1.minute + t2.minute
    total.second = t1.second + t2.second
    return total
```

This function is polite. It does not touch `t1` or `t2` at all — it just reads their values and builds a brand new `Time` object, which it returns. That is the definition of a **pure function**: it takes inputs in, sends a result out, and never modifies anything outside itself.

But test it on a 9:45 start and a 1:35 duration:

```python
start = Time()
start.hour = 9; start.minute = 45; start.second = 0

duration = Time()
duration.hour = 1; duration.minute = 35; duration.second = 0

done = add_time(start, duration)
print_time(done)
# → 10:80:00
```

`10:80:00` — there is no such time. The function added 45 and 35 minutes and got 80, without checking whether minutes exceeded 59. We need to **carry**, just like adding 45 + 35 on paper:

```python
def add_time(t1, t2):
    total = Time()
    total.hour   = t1.hour   + t2.hour
    total.minute = t1.minute + t2.minute
    total.second = t1.second + t2.second

    if total.second >= 60:
        total.second -= 60
        total.minute += 1

    if total.minute >= 60:
        total.minute -= 60
        total.hour   += 1

    return total
```

This works — but it's getting bulky, and it still only handles one overflow at a time. Add a 90-second duration and it would need to carry twice, growing again. This feeling of patching one bug and immediately worrying about the next is exactly what's called **prototype and patch** — a warning sign that a better approach exists.

> **Tip:** 

## Modifiers

A **modifier** is the other kind of function — one that changes the object you give it. Instead of politely building a new one, it reaches into the object you handed over and rewrites its contents:

```python
def increment(time, seconds):
    time.second += seconds

    if time.second >= 60:
        time.second -= 60
        time.minute += 1

    if time.minute >= 60:
        time.minute -= 60
        time.hour   += 1
```

After calling `increment(start, 30)`, `start` itself has changed — no new object was created. The function returns nothing because it does not need to; the change already happened to the thing you gave it.

> **Danger — modifiers and aliasing:** 

There's also a hidden flaw: if `seconds` is 90, `second` might still be `>= 60` after a single carry, since the `if` only fires once. You could swap `if` for `while`, but that's still patching. There's a much smarter fix.

## Prototype and patch vs. planned development

Writing the obvious thing, testing it, and patching whatever broke is **prototype and patch**. It's fast to start, but it has a nasty habit of producing complicated, fragile code full of special cases. The alternative is **planned development**: step back and ask, "what is this thing, really?"

Here's the insight: a time of day is just a number. 11 hours, 59 minutes, 30 seconds is really:

```text
11 × 3600 + 59 × 60 + 30 = 43170 seconds since midnight
```

Time is base-60 arithmetic — hours, minutes, and seconds are just columns in a base-60 number system, the way ones, tens, and hundreds are columns in base-10. Once you see that, convert `Time` to a plain integer, do normal arithmetic (which Python already knows how to do perfectly), and convert back:

```python
def time_to_int(time):
    minutes = time.hour * 60 + time.minute
    seconds = minutes * 60 + time.second
    return seconds


def int_to_time(seconds):
    time = Time()
    minutes, time.second = divmod(seconds, 60)
    time.hour, time.minute = divmod(minutes, 60)
    return time
```

`divmod` is a built-in that divides the first argument by the second and returns both quotient and remainder as a tuple — `divmod(130, 60)` gives `(2, 10)`. It handles the carrying for you, without any `if` or `while`. Now `add_time` becomes:

```python
def add_time(t1, t2):
    seconds = time_to_int(t1) + time_to_int(t2)
    return int_to_time(seconds)
```

Three lines. No `if` statements, no overflow checks, no special cases — and it handles any possible input correctly, not just the easy ones.

> **Tip:** 

## Invariants and valid_time

An **invariant** is a condition that should _always_ be true about an object — a contract it makes with the world. For a `Time` object: `hour` is non-negative, `minute` and `second` are each between 0 and 59. If violated, the object is lying — it claims to be a time, but describes a moment that does not exist.

```python
def valid_time(time):
    if time.hour < 0 or time.minute < 0 or time.second < 0:
        return False
    if time.minute >= 60 or time.second >= 60:
        return False
    return True
```

Call this at the top of your functions to catch problems early — before they silently corrupt something further down the line. Even better, use Python's `assert` statement:

```python
def add_time(t1, t2):
    assert valid_time(t1) and valid_time(t2)
    seconds = time_to_int(t1) + time_to_int(t2)
    return int_to_time(seconds)
```

`assert` says: this must be true, or crash immediately with an `AssertionError`. It makes your assumptions visible inside the code itself, instead of a comment that says "time must be valid here."

> **Watch out:** 

## Common mistakes

- Writing overflow checks with a single `if` that only fires once, missing multi-step carries
- Confusing a pure function (returns a new object) with a modifier (changes the original)
- Not realizing a modifier's changes persist through aliases pointing to the same object
- Reaching for `assert` to validate untrusted user input instead of `raise ValueError`

> **Practice with Sythra:**  [Practice with AI tutor](https://app.sythra.ai/pricing)

## FAQ

### What is a pure function in Python?

A pure function takes inputs, computes a result, and returns it without modifying the objects it was given or causing any side effects. This makes pure functions easy to test and safe to call anywhere.

### What is a modifier function in Python?

A modifier is a function that changes the object passed into it directly, rather than returning a new object. Because objects are passed by reference, the caller's original object reflects the change after the function returns.

### What does divmod() do in Python?

divmod(a, b) returns a tuple of (a // b, a % b) — the quotient and remainder of dividing a by b. It's useful for converting a total (like seconds) into components (like minutes and leftover seconds) without manual overflow checks.

### What is an invariant in programming?

An invariant is a condition that should always hold true for an object, such as a Time object's minute always being between 0 and 59. Checking invariants — often with assert — catches corrupted data early, before it causes confusing bugs elsewhere.

### When should I use assert instead of raise ValueError?

Use assert to catch programmer errors — bugs in your own code that should never happen if the code is correct. Use raise ValueError (or similar) to validate untrusted external input, like user-provided data, since assert statements can be disabled with Python's -O flag.

## Related

- [Classes and Objects in Python](https://app.sythra.ai/learn/python/classes-and-objects-python) — Defining classes and attributes — the foundation this builds on.
- [Classes and Methods in Python (self, __init__, Operator Overloading)](https://app.sythra.ai/learn/python/classes-and-methods-python) — self, __init__, __str__, and operator overloading.
- [Functions in Python in Depth: The Complete Guide](https://app.sythra.ai/learn/python/python-functions-in-depth) — The full picture of parameters, returns, and scope.
- [Python course hub](https://app.sythra.ai/learn/python) — All free Python explainers and the path into Agentic practice.

---

Written by Sythra — Learn machine learning by building. Practice this topic with Sythra's AI tutor: https://app.sythra.ai/pricing
