Typorithm · medium

Monkeypatch environment without leaks

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

Step 1 of 4 · intro card

Monkeypatch environment without leaks

0%

Concept and contract

Control present and missing settings for one test at a time.

Environment variables are process-wide inputs. A test that assumes the developer’s machine has a particular value is not self-contained. Explicitly set the value for the test that needs it, and explicitly remove it for a missing-value case.

monkeypatch records changes and restores them when the fixture scope ends. That includes restoring an original value rather than blindly deleting it. The test does not need to know whether the environment was initially populated to exercise a controlled branch.

Mechanism and worked example

The worked function reads a synthetic APP_MODE setting at call time, defaulting to development.

The worked function reads a synthetic APP_MODE setting at call time, defaulting to development. One test sets production and checks the returned value; another removes the key and checks the documented fallback. Both use monkeypatch so they do not leak state into each other.

Reading at call time is important here. If the application stores os.getenv in a module-level constant during import, changing the environment afterward does not rewrite that constant. Test the actual lookup boundary or deliberately structure configuration loading as an explicit function.

Complete worked example

python

import os


def app_mode():
    return os.getenv("APP_MODE", "development")


def test_explicit_mode(monkeypatch):
    monkeypatch.setenv("APP_MODE", "production")
    assert app_mode() == "production"


def test_missing_mode(monkeypatch):
    monkeypatch.delenv("APP_MODE", raising=False)
    assert app_mode() == "development"
Reasoning
The worked function reads a synthetic APP_MODE setting at call time, defaulting to development. One test sets production and checks the returned value; another removes the key and checks the documented fallback. Both use monkeypatch so they do not leak state into each other. Reading at call time is important here. If the application stores os.getenv in a module-level constant during import, changing the environment afterward does not rewrite that constant. Test the actual lookup boundary or deliberately structure configuration loading as an explicit function.
Open in local Python playground ↗

Focused test decision: context is in the complete example

python

monkeypatch.setenv("APP_MODE", "production")
assert app_mode() == "production"
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 function ignores the environment and always returns development.

The controlled function ignores the environment and always returns development. The production test catches it. An integration test that writes os.environ directly and forgets to restore it can pass on its own but change the behavior of later tests.

Use delenv(..., raising=False) when absence is a valid setup regardless of the machine’s starting state. Do not patch every environment key, and never print secrets to diagnose a failure. Our example uses a synthetic setting and no credentials or external connection.

Controlled bug: the tests must report failure

python

import os


def app_mode():
    return "development"


def test_explicit_mode(monkeypatch):
    monkeypatch.setenv("APP_MODE", "production")
    assert app_mode() == "production"


def test_missing_mode(monkeypatch):
    monkeypatch.delenv("APP_MODE", raising=False)
    assert app_mode() == "development"
Reasoning
The controlled function ignores the environment and always returns development. The production test catches it. An integration test that writes os.environ directly and forgets to restore it can pass on its own but change the behavior of later tests. Use delenv(..., raising=False) when absence is a valid setup regardless of the machine’s starting state. Do not patch every environment key, and never print secrets to diagnose a failure. Our example uses a synthetic setting and no credentials or external connection.
Open in local Python playground ↗

Independent practice

Add an explicitly empty value case.

Add an explicitly empty value case. The current contract preserves an empty string; a missing variable and an empty variable are different inputs. Set APP_MODE to the empty string and expect the empty string.

Do not assume a fallback with or "development" is equivalent to getenv’s default argument. The former treats an empty value as absent, while the latter uses the default only when the key is missing. Predict this difference before running the probe.

Step 1: Add a test distinguishing an explicitly empty setting from a missing setting. 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 os


def app_mode():
    return os.getenv("APP_MODE", "development")


def test_explicit_mode(monkeypatch):
    monkeypatch.setenv("APP_MODE", "production")
    assert app_mode() == "production"


def test_missing_mode(monkeypatch):
    monkeypatch.delenv("APP_MODE", raising=False)
    assert app_mode() == "development"

# Task: Add a test distinguishing an explicitly empty setting from a missing setting.
Reasoning
Add a test distinguishing an explicitly empty setting from a missing setting.
Open in local Python playground ↗
  • Hint 1: Set the key to "", rather than deleting it.
  • Hint 2: The current documented policy preserves that value.

Solution, tests and trade-offs

The solution adds the empty-string test.

The solution adds the empty-string test. Its practice mutation adds or "development" to the lookup. Production and missing settings still pass; only the explicit empty setting exposes the changed semantics.

For a real configuration API an empty value might instead be rejected. That is also a valid design when stated clearly, but it requires a different contract and corresponding exception tests. The lesson is to separate the cases rather than silently choosing one through Python truthiness.

Practice solution and acceptance checks

python

import os


def app_mode():
    return os.getenv("APP_MODE", "development")


def test_explicit_mode(monkeypatch):
    monkeypatch.setenv("APP_MODE", "production")
    assert app_mode() == "production"


def test_missing_mode(monkeypatch):
    monkeypatch.delenv("APP_MODE", raising=False)
    assert app_mode() == "development"

def test_empty_mode_is_preserved(monkeypatch):
    monkeypatch.setenv("APP_MODE", "")
    assert app_mode() == ""
Reasoning
The solution adds the empty-string test. Its practice mutation adds or "development" to the lookup. Production and missing settings still pass; only the explicit empty setting exposes the changed semantics. For a real configuration API an empty value might instead be rejected. That is also a valid design when stated clearly, but it requires a different contract and corresponding exception tests. The lesson is to separate the cases rather than silently choosing one through Python truthiness.
Open in local Python playground ↗

Interview reasoning and next step

Explain the restoration guarantee of the monkeypatch fixture and the difference between missing and empty values.

Explain the restoration guarantee of the monkeypatch fixture and the difference between missing and empty values. Discuss import-time versus call-time configuration and why a test should patch the place that actually owns the lookup.

Next replace a callable dependency using the same discipline. A fake should describe the boundary’s contract without performing network calls, sleeping or importing a production client just to make a unit test pass.

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-environment 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.

Control present and missing settings for one test at a time.