Typorithm · medium

Deterministic clocks and owned random state

pythonpytesttestingindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Deterministic clocks and owned random state

0%

Concept and contract

Inject time and a random generator instead of sleeping or seeding global state.

A flaky test gives different outcomes without an intentional change to the behavior under test. Wall-clock boundaries, arbitrary sleeps and shared random state are common causes. Make those inputs explicit when the code’s design permits it.

An expiry calculation can accept a clock callable. A sampling function can accept its own Random instance. The test then controls both without changing the real system clock or resetting a global generator used by another component.

Mechanism and worked example

The worked expiry helper adds a fixed lifetime to an injected clock.

The worked expiry helper adds a fixed lifetime to an injected clock. A lambda supplies a known instant, so the expected expiry is literal and deterministic. The sampling test constructs two independent generators with the same seed and compares their outputs.

The sampling assertion demonstrates reproducibility within this tested runtime, not a promise that every Python version or distribution uses an eternally identical sequence. For algorithmic correctness also assert properties such as population membership and count, rather than treating a seed as the entire specification.

Complete worked example

python

import random


def expires_at(ttl, clock):
    return clock() + ttl


def choose_two(rng):
    return rng.sample(["python", "sql", "pytest"], 2)


def test_expiry_uses_injected_time():
    assert expires_at(10, lambda: 100) == 110


def test_owned_generators_repeat():
    assert choose_two(random.Random(7)) == choose_two(random.Random(7))
Reasoning
The worked expiry helper adds a fixed lifetime to an injected clock. A lambda supplies a known instant, so the expected expiry is literal and deterministic. The sampling test constructs two independent generators with the same seed and compares their outputs. The sampling assertion demonstrates reproducibility within this tested runtime, not a promise that every Python version or distribution uses an eternally identical sequence. For algorithmic correctness also assert properties such as population membership and count, rather than treating a seed as the entire specification.
Open in local Python playground ↗

Focused test decision: context is in the complete example

python

assert expires_at(10, lambda: 100) == 110
Focused test decision
Type this short test decision after understanding the complete runnable module. Desktop typing records practice; Run tests in the local playground executes real pytest.

Counterexamples and common mistakes

The controlled expiry helper subtracts the lifetime.

The controlled expiry helper subtracts the lifetime. Its deterministic test fails immediately without waiting. A sleep-based test could instead be slow or intermittently cross a scheduling boundary. Increasing the sleep after each failure hides rather than diagnoses that uncontrolled dependency.

Global random.seed can affect other tests and application code. Recreate the owned generator for each independent test. If input order should be irrelevant, test that invariant directly; do not assume a fixed seed repairs an order-sensitive algorithm.

Controlled bug: the tests must report failure

python

import random


def expires_at(ttl, clock):
    return clock() - ttl


def choose_two(rng):
    return rng.sample(["python", "sql", "pytest"], 2)


def test_expiry_uses_injected_time():
    assert expires_at(10, lambda: 100) == 110


def test_owned_generators_repeat():
    assert choose_two(random.Random(7)) == choose_two(random.Random(7))
Reasoning
The controlled expiry helper subtracts the lifetime. Its deterministic test fails immediately without waiting. A sleep-based test could instead be slow or intermittently cross a scheduling boundary. Increasing the sleep after each failure hides rather than diagnoses that uncontrolled dependency. Global random.seed can affect other tests and application code. Recreate the owned generator for each independent test. If input order should be irrelevant, test that invariant directly; do not assume a fixed seed repairs an order-sensitive algorithm.
Open in local Python playground ↗

Independent practice

Add the zero-lifetime expiry boundary with the same injected clock.

Add the zero-lifetime expiry boundary with the same injected clock. Expect the current instant exactly. Keep the original positive lifetime test. Then inspect a probe that imposes a minimum lifetime of one, which is a plausible but undocumented policy change.

The exercise changes one assertion at a time. Do not run many random trials and hope that a boundary appears. A deterministic example of the boundary is both faster and easier to explain.

Step 1: Add the zero-lifetime case with an injected fixed clock. Step 2: Keep the existing passing cases. Add the smallest new assertion first; predict the failure before running pytest.

Practice starter: solve before reading the solution

python

import random


def expires_at(ttl, clock):
    return clock() + ttl


def choose_two(rng):
    return rng.sample(["python", "sql", "pytest"], 2)


def test_expiry_uses_injected_time():
    assert expires_at(10, lambda: 100) == 110


def test_owned_generators_repeat():
    assert choose_two(random.Random(7)) == choose_two(random.Random(7))

# Task: Add the zero-lifetime case with an injected fixed clock.
Reasoning
Add the zero-lifetime case with an injected fixed clock.
Open in local Python playground ↗
  • Hint 1: Use the same instant to isolate the changed lifetime.
  • Hint 2: No sleep or wall-clock read is needed.

Solution, tests and trade-offs

The solution checks the zero lifetime.

The solution checks the zero lifetime. The practice mutation adds max(ttl, 1), leaving the positive ten-unit case unchanged. The new assertion detects the altered boundary. The sampling check separately verifies repeatability with owned state.

This suite does not test concurrent scheduling, timer interrupts or operating-system monotonic-clock guarantees. For real timeouts use an appropriate monotonic clock and integration tests. Distinguish controlling time for a pure calculation from proving a deployed scheduler is punctual.

Practice solution and acceptance checks

python

import random


def expires_at(ttl, clock):
    return clock() + ttl


def choose_two(rng):
    return rng.sample(["python", "sql", "pytest"], 2)


def test_expiry_uses_injected_time():
    assert expires_at(10, lambda: 100) == 110


def test_owned_generators_repeat():
    assert choose_two(random.Random(7)) == choose_two(random.Random(7))

def test_zero_lifetime_expires_now():
    assert expires_at(0, lambda: 100) == 100
Reasoning
The solution checks the zero lifetime. The practice mutation adds max(ttl, 1), leaving the positive ten-unit case unchanged. The new assertion detects the altered boundary. The sampling check separately verifies repeatability with owned state. This suite does not test concurrent scheduling, timer interrupts or operating-system monotonic-clock guarantees. For real timeouts use an appropriate monotonic clock and integration tests. Distinguish controlling time for a pure calculation from proving a deployed scheduler is punctual.
Open in local Python playground ↗

Interview reasoning and next step

Explain why injected inputs make tests deterministic and why retries are not a substitute for understanding flakes.

Explain why injected inputs make tests deterministic and why retries are not a substitute for understanding flakes. Distinguish reproducibility from correctness and a stable regression case from a random exploration tool.

Next combine these ideas in a small capstone. A useful suite has named contracts, independent expected values, clear failure phases and deliberate checks that its assertions can reject a plausible defect. Preserve an independently reasoned failing input when exploration finds a bug; that small regression case should run without depending on chance.

Optional stretch — after the core task works: Introduce one deliberate implementation mistake and explain which assertion catches it. A green run alone is not evidence that every possible bug is covered.

Computer lab: https://applydjinn.com/typorithm/pytest-lab-20261106.zip. Run verify.py in an isolated environment. Worked examples and solutions pass, controlled counterexamples fail, and the new practice assertions reject a mutation missed by the baseline. Run --practice pyq-determinism after changing the supplied exercise. Python executes on your computer or in the browser, without submitting code to a server. Native question progress is a separate learning record.

Inject time and a random generator instead of sleeping or seeding global state.