Do not extract helpers from an unpinned god function. Record env, cache, and quote maps first. Then move only one pure rate selector. That order keeps the public quote contract stable.
Messy production repos rarely fail at simple syntax. They fail when a tiny extract changes hidden behavior. Env flags, module caches, and debug files leak into results. A characterization harness makes those leaks visible before the split.
Why extract second, not first
Refactor instinct says extract a tax helper now. That instinct is usually too early. You still lack a tape of current outputs. Rounding, cache keys, and promo flags can drift without a compile error.
Characterization tests do not prove the policy is correct. They prove today's observables stay still. That is the only safe baseline for a god function. Change one pure helper only after that baseline is green.
What this workflow pins
Pin five observables, not the source shape. Pin the environment variables the quote actually reads. Pin cache keys and value types after each call. Pin whether a debug file was created. Pin quote map keys and quantized decimal strings. Pin exception types for bad regions and quantities.
Do not pin wall-clock timestamps as object identity. Do not pin unfiltered filesystem trees after the call. Do not pin log lines that embed process ids. Those fields churn without a real behavior change. They make the harness flaky and unreadable.
Artifact: a self-contained pricer tape
The listing below is a proposed local example. Save both files, then run the tests yourself. Treat results as machine evidence, not published benchmarks. Reset env, cache, and debug files between cases.
File pricing.py
"""Messy quote module. Pin behavior before any extract."""
from __future__ import annotations
import json
import os
from decimal import Decimal, ROUND_HALF_EVEN
from pathlib import Path
from typing import Any
CACHE: dict[str, Any] = {}
DEBUG_NAME = "quote_debug.json"
class QuoteError(ValueError):
"""Invalid region or quantity for a quote."""
def build_quote(region: str, quantity: int, sku: str) -> dict[str, Any]:
promo = os.environ.get("QUOTE_PROMO", "off")
debug_on = os.environ.get("QUOTE_DEBUG", "") == "1"
if quantity < 1:
raise QuoteError("quantity must be >= 1")
if region not in {"US", "EU", "JP"}:
raise QuoteError(f"unsupported region: {region}")
cache_key = f"{region}:{sku}:{quantity}:{promo}"
if cache_key in CACHE:
CACHE["_hits"] = int(CACHE.get("_hits", 0)) + 1
return CACHE[cache_key]
base = Decimal("19.99")
if sku.startswith("PRO-"):
base = Decimal("49.00")
elif sku.startswith("LAB-"):
base = Decimal("12.50")
# Mixed policy: region, promo flag, and arithmetic live together.
if region == "US":
rate = Decimal("0.05") if promo == "on" else Decimal("0.08")
elif region == "EU":
rate = Decimal("0.20")
else:
rate = Decimal("0.10")
subtotal = (base * Decimal(quantity)).quantize(
Decimal("0.01"), rounding=ROUND_HALF_EVEN
)
tax = (subtotal * rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN)
total = (subtotal + tax).quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN)
quote = {
"sku": sku,
"region": region,
"quantity": quantity,
"promo": promo,
"tax_rate": str(rate),
"subtotal": str(subtotal),
"tax": str(tax),
"total": str(total),
"cached": False,
}
if debug_on:
Path(DEBUG_NAME).write_text(
json.dumps(quote, indent=2), encoding="utf-8"
)
stored = {**quote, "cached": True}
CACHE[cache_key] = stored
CACHE["_hits"] = int(CACHE.get("_hits", 0))
return quote
File test_pricing_char.py
"""Characterization tape for build_quote. Run before any extract."""
from __future__ import annotations
import os
import unittest
from pathlib import Path
import pricing
class BuildQuoteCharacterization(unittest.TestCase):
def setUp(self) -> None:
pricing.CACHE.clear()
os.environ.pop("QUOTE_PROMO", None)
os.environ.pop("QUOTE_DEBUG", None)
Path(pricing.DEBUG_NAME).unlink(missing_ok=True)
def tearDown(self) -> None:
Path(pricing.DEBUG_NAME).unlink(missing_ok=True)
pricing.CACHE.clear()
def test_us_list_rate_and_cache_miss_shape(self) -> None:
quote = pricing.build_quote("US", 2, "PRO-1")
self.assertEqual(
quote["tax_rate"],
"0.08",
)
self.assertEqual(quote["subtotal"], "98.00")
self.assertEqual(quote["tax"], "7.84")
self.assertEqual(quote["total"], "105.84")
self.assertFalse(quote["cached"])
self.assertEqual(quote["promo"], "off")
self.assertIn("US:PRO-1:2:off", pricing.CACHE)
self.assertEqual(pricing.CACHE["_hits"], 0)
self.assertFalse(Path(pricing.DEBUG_NAME).exists())
def test_promo_env_changes_us_rate_only(self) -> None:
os.environ["QUOTE_PROMO"] = "on"
quote = pricing.build_quote("US", 2, "PRO-1")
self.assertEqual(quote["tax_rate"], "0.05")
self.assertEqual(quote["tax"], "4.90")
self.assertEqual(quote["total"], "102.90")
self.assertIn("US:PRO-1:2:on", pricing.CACHE)
def test_cache_hit_flips_cached_and_hits(self) -> None:
first = pricing.build_quote("EU", 1, "LAB-9")
second = pricing.build_quote("EU", 1, "LAB-9")
self.assertFalse(first["cached"])
self.assertTrue(second["cached"])
self.assertEqual(second["tax_rate"], "0.20")
self.assertEqual(pricing.CACHE["_hits"], 1)
def test_debug_env_writes_json_once(self) -> None:
os.environ["QUOTE_DEBUG"] = "1"
pricing.build_quote("JP", 3, "STD-2")
path = Path(pricing.DEBUG_NAME)
self.assertTrue(path.is_file())
text = path.read_text(encoding="utf-8")
self.assertIn('"region": "JP"', text)
self.assertIn('"tax_rate": "0.10"', text)
def test_invalid_region_and_quantity_types(self) -> None:
with self.assertRaises(pricing.QuoteError) as region_err:
pricing.build_quote("UK", 1, "PRO-1")
self.assertIn("unsupported region", str(region_err.exception))
with self.assertRaises(pricing.QuoteError) as qty_err:
pricing.build_quote("US", 0, "PRO-1")
self.assertIn("quantity must be", str(qty_err.exception))
self.assertEqual(pricing.CACHE, {})
if __name__ == "__main__":
unittest.main()
Run the tape with a clean working directory. Keep the debug filename local to the test process.
python -m unittest test_pricing_char.py -v
You should see five passing tests on this example. If a test fails, stop the extract. The tape is lying or the module drifted.
Python decimal rounding is defined in the standard library. Use banker's rounding only if the current module already does. See the decimal documentation before you change quantize calls.
Decision table for the first extract
Use this table to reject oversized splits. Each row is one observable. The extract may touch source lines. It may not change the pin.
| Observable | How you pin it | Allowed to change on extract |
|---|---|---|
QUOTE_PROMO default |
missing env equals off
|
no |
| US list tax rate | tax_rate == "0.08" |
no |
| US promo tax rate | env on then "0.05"
|
no |
| EU and JP rates |
"0.20" and "0.10"
|
no |
| Money fields | quantized strings to 0.01
|
no |
| Cache key shape | region:sku:qty:promo |
no |
| Cache hit flag |
cached True on second call |
no |
| Debug file | exists only when env is 1
|
no |
QuoteError type |
region and quantity paths | no |
Function name build_quote
|
public import still works | no |
The only allowed source change is mechanical. A rate selector returns the same Decimal the god function used. Call sites inside build_quote stay in process. No new files appear in the quote path.
Numbered workflow
Copy the god function into a throwaway branch. Do not rename public functions on that branch. Keep import paths identical for callers.
List every branch you can see in five minutes. Env defaults, sku prefixes, and region arms all count. Invalid inputs count as branches too.
Write one test per branch before editing production lines. Assert maps, cache keys, files, and exception types. Avoid asserting entire
repr()strings of modules.Reset globals in
setUpandtearDownwithout fail. A dirtyCACHEpoisons later cases. A leftover debug file poisons later cases.Run the tape until it is green twice. Then extract one function that returns a
Decimalrate. Keepbuild_quoteas the only public entry.Re-run the same tape with no assertion edits. If you must edit an assertion, the extract was not behavior-preserving. Revert and shrink the change.
Smallest safe extract after the tape
Proposed extract only. Execute the tape before and after this edit.
def select_tax_rate(region: str, promo: str) -> Decimal:
if region == "US":
return Decimal("0.05") if promo == "on" else Decimal("0.08")
if region == "EU":
return Decimal("0.20")
if region == "JP":
return Decimal("0.10")
raise QuoteError(f"unsupported region: {region}")
Inside build_quote, replace the inline rate block with one call. Keep the unsupported region check where it already runs. Do not move cache writes into the selector. Do not move debug I/O into the selector.
rate = select_tax_rate(region, promo)
That is the entire production diff. Sku base prices stay in build_quote. Quantize calls stay in build_quote. Env reads stay in build_quote. The selector is pure and locally obvious.
Using a free model only for missing branches
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
A free model can read pricing.py and list untested arms. MonkeyCode free model access and the free server option can draft extra cases from those arms. You still paste candidates into test_pricing_char.py. You still run unittest on your machine.
Do not let the model rewrite assertions to match a new extract. That hides a contract break. Do not paste secrets from real environments into any prompt. The harness above uses only dummy promo flags.
What still breaks if you skip a pin
Skip env isolation and US promo quotes look like list quotes. Skip cache clears and hit counters leak across tests. Skip debug cleanup and later cases see stale files. Skip exception types and callers catch the wrong class.
Skip quantized strings and float noise appears later. 8 * 0.08 is not a money contract. The current module already chose Decimal strings. The tape must keep that choice frozen.
Limitations
Characterization freezes bugs beside features. A wrong JP rate stays wrong after a clean extract. Do not use this tape as a tax-law oracle. Replace pins only when product owners change the contract.
The harness assumes in-process globals. A remote cache or shared temp disk needs different fixtures. The harness assumes deterministic arithmetic. Live FX feeds do not belong in this design.
The example writes quote_debug.json in the working directory. That is convenient and also dangerous. Point debug I/O at a temp dir before production use. Do not commit debug files from failed local runs.
Who should not use this approach
Skip this workflow on a greenfield pricer with no callers. You can design a pure module from day one. Skip it when the ticket requires new rates. A behavior change needs new assertions, not a frozen tape.
Skip it when quotes call the network inside the god function. Pin those calls with fakes first, then extract. Skip it when you cannot reset env and cache in tests. Unisolated tests will endorse racey lies.
Skip it for secret-bearing environments. Promo flags in this example are dummy strings. Real tokens must never enter fixtures or model prompts.
Close the loop
Keep the public build_quote map stable across the split. Move one pure Decimal selector, then stop. Re-run the same five pins without editing them. If a pin moves, the extract was too large.
If a god pricer is blocking a split, MonkeyCode free model access and the free server option can draft extra characterization cases from the source. Run every case on your machine before you extract.
Top comments (0)