Double-Entry
Ledger
sum(entries) == 0.
Every journal entry is a Debit or a Credit object that knows how to negate, add, compare and hash itself.
Balance the books with one expression: sum(entries) == 0.
No helper functions, no .total() β just operators.
This live project is the operator overloading entry in the Polymorphism series.
It uses __neg__, __add__, __eq__, __hash__ and __repr__
so that ordinary Python arithmetic drives a real accounting model.
π― By the end you can…
- Explain debits, credits and why books balance
- Read every line of the ledger program
- Write
__add__,__neg__,__eq__,__hash__correctly - Use
Decimaland frozen dataclasses for money - Debug the
sum()seed problem - Test operators with
unittest
π You should already know
- Functions, lists and dictionaries
- What a class and a method are (roughly)
Dataclasses, Decimal and operator methods are taught in the primer; accounting is taught in section 01.
What Is a Double-Entry Ledger?
π Every Entry Has Two Sides
Accounting's golden rule: every transaction affects at least two accounts. If you receive cash, you also record where it came from. Debits and credits must balance to zero β always. If they don't, something is wrong with your books.
In this project, a Debit is a positive amount and a
Credit is the exact same amount, negated. Because __add__
and __neg__ are implemented, adding an entry to its opposite produces exactly zero.
A ledger is balanced when sum(entries) == 0.
This is polymorphism through operator overloading: the call site
sum(entries) doesn't know or care whether it's holding Debits, Credits, or both.
It just calls +, and each object answers with its own arithmetic.
The Shape of the Data
A Debit holds a positive amount. A Credit is a Debit that has been negated β literally produced by -debit. Both are entries. Both know how to add, compare and hash themselves. A transaction holds two entries and a description.
Note on the design: a Credit is just a Debit with a negative amount. This keeps the ledger simple β we don't need two classes to get two behaviors. The operator does the differentiation.
π Accounting in five minutes
You do not need a business background for this page, only five ideas. Double-entry bookkeeping was described in print by Luca Pacioli in 1494 (although merchants used it earlier), and it has survived five centuries because it makes mistakes detectable: every event is written down twice, from two points of view, and the two must agree.
- Accounts are labelled buckets of money:
Cash,Supplies,Sales Revenue. - A transaction is one business event. It always touches at least two accounts.
- A debit is an entry on the left side of an account; a credit is an entry on the right. The words are bookkeeping labels, not “good” and “bad”.
- The golden rule: for every transaction, total debits equal total credits.
- The ledger is the collection of all accounts. If the golden rule held for every transaction, the whole ledger balances.
Which side makes an account grow?
| Account type | Examples | A debit… | A credit… |
|---|---|---|---|
| Asset | Cash, Supplies, Accounts Receivable | increases it | decreases it |
| Expense | Rent, Wages | increases it | decreases it |
| Liability | Loans, Accounts Payable | decreases it | increases it |
| Equity | Owner’s Equity | decreases it | increases it |
| Revenue | Sales Revenue, Service Revenue | decreases it | increases it |
These rules come from the accounting equation Assets = Liabilities + Equity, where revenue adds to equity and expenses subtract from it.
The three sample transactions in bookkeeping language
| Event | Debit | Credit | Amount | Why |
|---|---|---|---|---|
| Cash sale | Cash | Sales Revenue | 250.00 | An asset grows (cash in) and revenue is earned. |
| Office supplies | Supplies | Cash | 75.50 | One asset grows, another shrinks: we swapped cash for supplies. |
| Consulting | Cash | Service Revenue | 1,200.00 | Cash in, service revenue earned. |
The same events drawn as T-accounts
A T-account has a debit (left) column and a credit (right) column. Reading each account’s two columns gives its balance.
1,200.00
Total of the debit balances: 1,374.50 + 75.50 = 1,450.00. Total of the credit balances: 250.00 + 1,200.00 = 1,450.00. Equal — this check is called a trial balance. The program below does the same arithmetic with nothing but a dictionary and a loop.
Modelling choice you should notice: this project stores one signed number: debit = positive, credit = negative. That is why Sales Revenue shows −250.00 in the code even though revenue is good news — a report would flip the sign based on the account type. Real systems often store separate debit and credit columns or integer cents; signed amounts are simply the neatest fit for operator overloading.
Balanced does not mean correct. An invariant that total debits equal total credits catches one-sided entries and arithmetic slips. It does not catch posting 250 to the wrong account on both sides, or forgetting a whole transaction. Treat sum == 0 as a smoke alarm, not a guarantee.
Three Python Ideas Behind Every Line
The ledger uses three features that beginners often meet for the first time here: dataclasses, Decimal and operator methods. If you already know them, skim the cards and jump to the live demo. If not, each idea has a short program that you can run and read line by line.
How to study with this page. β Predict: open “Predict first” and guess the output before pressing Run. β‘ Run the program. β’ Read the line-by-line explanation: tap any line, or switch on Step mode and press Next to walk through one line at a time while the matching line lights up. β£ Change one thing in the playground and predict again.
Dataclass
A decorator that writes __init__, __repr__ and __eq__ from a list of fields. With frozen=True the object becomes read-only.
Decimal
Exact base-10 arithmetic for money. Build it from a string: Decimal("0.10"), never from a float.
Dunder methods
Methods with double underscores (“dunder”) that Python calls for operators and built-ins: a + b runs a.__add__(b).
π¦ Program A — what a dataclass writes for you
The same two-field class written three ways: by hand, as a dataclass, and as a frozen dataclass. Compare what print and == do for each.
Takeaway: @dataclass removes boilerplate, and frozen=True adds the safety a ledger entry needs: once created it cannot be changed, and it can be used in sets.
π° Program B — why money needs Decimal
The reason the ledger imports Decimal in its very first lines.
Rule of thumb: floats are for measurements (height, temperature); Decimal or integer cents are for money. the standard-library documentation presents Decimal as the tool for applications that need exact decimal representation, accounting being the classic case.
β Program C — operators are method calls
Watch Python choose between __add__, __radd__ and TypeError. This is the exact protocol the Debit class relies on.
πΊοΈ Operator → method cheat sheet
| You write | Python calls | Used in the ledger? |
|---|---|---|
a + b | a.__add__(b), then b.__radd__(a) if that returns NotImplemented | Yes — adds entries of one account |
-a | a.__neg__() | Yes — creates a credit |
a == b | a.__eq__(b) | Yes — compares account and amount |
a != b | a.__ne__(b) (by default the inverse of __eq__) | Automatic |
hash(a), {a}, d[a] | a.__hash__() | Yes — sets and dict keys |
print(a), repr(a) | a.__repr__() (or __str__ if defined) | Yes — readable output |
a - b | a.__sub__(b) | Challenge 2 |
a < b | a.__lt__(b) | Suggested in the outro |
abs(a), bool(a), len(a) | __abs__, __bool__, __len__ | Ideas to explore |
sum(items) | Starts at start (default 0) and applies + repeatedly | Yes — see the Bug Hunt |
One technical detail: Python looks these methods up on the type of the object, not on the instance. Assigning obj.__add__ = ... on one object does not change how + behaves for it.
Double-Entry Ledger
A working ledger. Pick a debit account, a credit account and an amount, then post the transaction. Watch the journal, the per-account balances (a trial balance) and the running total update. Every transaction posts two entries, +amount and −amount, so the total stays at zero — until you press Corrupt the books, which simulates a bug that records only one side.
Journal
| # | Description | Debit | Credit | Net |
|---|---|---|---|---|
| No entries yet β post a transaction above. | ||||
Account balances (trial balance)
| Account | Balance | Side |
|---|---|---|
| β | ||
What to notice: (1) Each posted transaction adds a +amount entry to one account and a −amount entry to another, so the total never moves from zero. (2) The balances table is a trial balance: its Ξ£ is the same number as the ledger total. (3) After Corrupt the books, the total is no longer zero and the trace shows by how much — that is the smoke alarm described in the introduction.
Experiments:
- Load the sample and check that the Cash balance matches the T-account in section 01 (
1,374.50). - Post Rent paid: debit
Rent Expense, creditCash, 500. Which balances change, and does the total? - Post a transaction whose debit and credit account are the same. Why does the page refuse it?
- Corrupt the books twice. Does the imbalance add up? Then clear the ledger.
The demo does its arithmetic in whole cents (integers), mirroring the exactness that Decimal gives the Python code. It is a JavaScript simulation of the Python model, not Python itself; the programs further down run the real thing.
How It Works β Line by Line
The full implementation of the ledger. Run it, then read it one line at a time in the walkthrough below.
The Ledger Class
π Deep dive β how sum(entries) stays at zero
| Entry | account | amount | Operator applied |
|---|---|---|---|
| Cash (debit) | Cash | +250.00 | +250.00 + (β250.00) = 0 |
| Sales Revenue (credit) | Sales Revenue | β250.00 | |
| Supplies (debit) | Supplies | +75.50 | +75.50 + (β75.50) = 0 |
| Cash (credit) | Cash | β75.50 | |
| Cash (debit) | Cash | +1200.00 | +1200.00 + (β1200.00) = 0 |
| Service Revenue (credit) | Service Revenue | β1200.00 |
A precision note about sum(entries) == 0. The page’s headline expression is the idea: the total of all signed entries is zero. In the code, the ledger-wide total adds the entry amounts (Decimal numbers), because Debit.__add__ deliberately refuses to combine different accounts. The + operator does its real work within one account — see account_balance() — and the Bug Hunt below explains why.
π Bug hunt: the __start__ seed
The first draft of this page computed the balance with sum(self._entries, start=Debit("__start__", Decimal("0"))) and explained that a neutral “start” account was needed because entries on different accounts cannot be added. Unfortunately the idea fails: that code raises ValueError: Cannot add __start__ to Cash on the very first addition, and no seed would fix it, because the second step then tries to add two different real accounts. The program below reproduces both failures and shows the fix. Detective work like this — read the error, find the first failing step — is a core debugging skill.
What the fix teaches: an operator you define has rules (same account only), and every function that uses it — including built-ins like sum — must respect them. When a rule does not fit a task (summing across accounts), change the data you feed in (use the amounts), not the rule.
π Summary: the design in fourteen sentences
@dataclass(frozen=True)β makesDebitimmutable. Once an entry is recorded, it can't be changed. That is exactly what a ledger needs.account: str,amount: Decimalβ the two fields.Decimalis used instead offloatbecause money arithmetic must be exact.__add__β first checks type:return NotImplementedifotherisn't aDebit. Then checks the account: adding to a different account raisesValueError. Finally returns a newDebit.__neg__β returns a newDebitwith the amount negated. This is what turns a debit into a credit.__eq__β compares(account, amount)tuples. ReturnsNotImplementedfor non-Debittypes, so Python can fall back to "not equal".__hash__β hashes the same(account, amount)tuple that__eq__compares. (A frozen dataclass would generate a matching pair by itself; writing both by hand shows the rule that equal objects must hash equally.)__repr__β shows the class name and fields. Makes debug output readable.Credit = Debitβ a credit is just a debit alias. The actual differentiation happens through the sign, which__neg__controls.Transactionβ a plain dataclass holding description, two account names, and an amount.Transaction.entries()β produces the pair: a debit for thedebit_accountand a credit (negated debit) for thecredit_account. The same amount, opposite signs.Ledger.post()β appends the transaction and extends the entry list with the two entries.Ledger.balance()βsum((e.amount for e in self._entries), start=Decimal("0")). We add the amounts becauseDebit.__add__refuses to combine different accounts, so a ledger-wide sum ofDebitobjects is impossible. The first draft used a__start__seed instead and crashed; see the Bug Hunt above.Ledger.is_balanced()βself.balance() == 0. That's it. The check is a single comparison.account_balance()β collects one account's entries and adds them with the+operator, seeded with a zero entry for the same account. This is where operator overloading does real work.total_debits()andtotal_credits()β split the entries by sign. These should be equal in a balanced ledger.
β οΈ Common mistakes
Raising TypeError inside __add__. Return NotImplemented. Let Python build the TypeError itself.
Using float for money. 0.1 + 0.2 is not 0.3. Use Decimal.
Forgetting @dataclass(frozen=True). Then a debit can be mutated after being recorded, and your ledger becomes unreliable.
Mismatched __eq__ and __hash__. Two equal entries must hash the same. Keep them consistent.
Mixing currencies in one ledger. Add a currency field to Debit and check it in __add__, or raise on mismatch. (See the Currency Converter for the full pattern.)
Every mistake above is reproduced as a runnable program in section 07.
π§ͺ Try it yourself
Change one thing at a time:
- Remove
@dataclass(frozen=True)and try to mutate a posted entry. What happens? - Add a
currencyfield and raiseValueErrorwhen currencies don't match. Try it. - Add a
Ledger.account_balance()call for "Cash" after the three transactions. What number do you expect? - Try
Debit("Cash", 10) + 5. Read the error.
β Answers to “Try it yourself”
Show answers and explanations
- Remove
frozen=Trueand mutate a posted entry. The assignment succeeds silently, and the ledger’s numbers change with no record of it. See Mistake 3 in section 07. (Careful: with a hand-written__hash__and a mutable object, changingamountafter putting the entry in a set breaks the set, because its hash changes.) - Add a currency field. Challenge 3 has a full solution.
account_balance("Cash")after the three transactions. 250.00 − 75.50 + 1,200.00 =1374.50. The main program now prints it.Debit("Cash", 10) + 5.__add__returnsNotImplemented,inthas no matching reflected method, so Python raisesTypeError: unsupported operand type(s) for +: 'Debit' and 'int'. You can reproduce it in the dunder lab below.
The Operators Behind the Ledger
Each dunder method hooks a Python operator into the Debit class. Here's how each one is used in the ledger.
β __add__
Hooks a + b. Adds two debits on the same account. Refuses cross-account addition.
Debit("Cash", 10) + Debit("Cash", 5)
# β Debit("Cash", 15)
Debit("Cash", 10) + Debit("AR", 5)
# β ValueErrorβ __neg__
Hooks -a. Returns a new Debit with the negated amount. This is how a credit is created.
-Debit("Sales", 250)
# β Debit("Sales", -250)
# (a credit)π° __eq__
Hooks a == b. Compares both the account and the amount.
Debit("Cash", 10) == Debit("Cash", 10)
# β True
Debit("Cash", 10) == Debit("Cash", 11)
# β Falseπ __hash__
Enables entries in sets and as dict keys. Must be consistent with __eq__.
{Debit("Cash", 10),
Debit("Cash", 10)}
# β {Debit("Cash", 10)}
# (one entry, deduplicated)π¬ Dunder lab — watch every operator run
The four cards above are reference snippets. This program executes every one of those examples, including the failures, so you can compare your prediction with real output. It is also the best place to experiment: change an amount or an account name and predict again.
βοΈ Design rules that keep operators trustworthy
- Return new objects.
a + bmust not modifyaorb. Withfrozen=TruePython enforces this for you. - Return
NotImplementedfor types you do not understand, and never raiseTypeErroryourself (Mistake 1). - Keep
__eq__and__hash__in agreement. Equal objects must hash equally (Mistake 4). - Make operators total or explicit. Either an operator works for every pair of values of the supported types, or it raises a clear, specific error (
ValueErrorfor different accounts). - Do not be clever. An operator should mean what a reader expects.
+that adds money is great;+that posts to a database is a trap.
Why sum(entries) == 0 Always Holds
The Proof
It comes down to one algebraic identity. For every transaction t with amount a:
entries(t) = (Debit(debit_account, a), Debit(credit_account, -a))
Sum of entries across all transactions:
Ξ£ entries = Ξ£ (a) + Ξ£ (-a)
= Ξ£ (a - a)
= Ξ£ (0)
= 0It's true by construction. Every transaction adds one positive and one negative of the same amount. The ledger's balance is zero before you even look at the number β the invariant is baked into how the data is created.
What You Get for Free
| Task | Code | What it uses |
|---|---|---|
| Balance the ledger | ledger.is_balanced() | __add__, __eq__ |
| Check one account | ledger.account_balance("Cash") | __eq__, __add__ |
| Deduplicate entries | set(entries) | __eq__, __hash__ |
| Group by account | groupby(entries, key=lambda e: e.account) | Attribute access |
| Find the largest entry | max(entries, key=lambda e: abs(e.amount)) | Attribute access |
Try adding a new feature: compute a trial balance per account. You can do it with a single dict comprehension β no new methods needed. Because Debit knows how to add itself, everything else composes naturally.
π² Test the proof on 1,000 random transactions
A proof is general; a test is concrete. This program generates a thousand random transactions with a fixed random seed, so the output is reproducible, and checks both the ledger total and the sum of the per-account balances. It ends with a reminder of why the amounts must be Decimal.
π§° Run every row of the table above
Each task in “What You Get for Free” is demonstrated below on a small set of entries. It deliberately includes a duplicate entry: duplicates are exactly how a real ledger drifts out of balance, and sets make them easy to find.
Do not “fix” the books with set(). Removing duplicate entries would delete legitimate repeated transactions (two identical cash sales are two sales). Use sets to find suspicious duplicates, then investigate.
π§ What the invariant can and cannot promise
| Kind of error | Caught by sum == 0? | Why |
|---|---|---|
| A one-sided entry (only a debit was recorded) | β Yes | The lonely amount has no partner. |
| A typo on one side (250 vs 205) | β Yes | The two sides differ. |
| The same typo on both sides | β No | Both sides still match. |
| Posting to the wrong account on both sides | β No | The totals are unaffected. |
| A forgotten transaction | β No | Nothing is unbalanced, merely missing. |
| A transaction posted twice (as a whole) | β No | Both copies balance. |
This is why real accounting also uses reconciliations, audit trails and review. The invariant makes one whole class of bugs impossible; it does not make the books right.
Four Classic Mistakes, Reproduced
The earlier “Common mistakes” list told you what not to do. Here you can watch each mistake fail. Run the program, read the symptom, then read why it happens. Predict the output first.
1 Β· Raising TypeError instead of returning NotImplemented
The bad version blocks Python’s second chance: asking the right-hand operand to handle the addition.
2 Β· Using float for money
Binary floating point cannot represent 0.10, so tiny errors appear and exact checks fail.
3 Β· Forgetting frozen=True
A posted entry that can be edited is a ledger that can be falsified without a trace.
4 Β· Mismatched __eq__ and __hash__
Three variants: equality alone (unhashable), a consistent pair, and an inconsistent pair that fails silently.
π©Ί Error-message decoder
| Message (shortened) | Most likely cause | Fix |
|---|---|---|
ValueError: Cannot add Cash to AR: different accounts | Two entries from different accounts were added. | Group by account first, or sum the .amount values. |
TypeError: unsupported operand type(s) for +: 'Debit' and 'int' | A Debit was added to a plain number. | Add .amount instead, or support the type deliberately (Challenge 4). |
TypeError: unsupported operand type(s) for +: 'int' and 'Debit' | sum(entries) without start=: it begins at the integer 0, and Debit has no __radd__. | Pass start=Debit(account, Decimal("0")) for one account, or sum the amounts. |
FrozenInstanceError: cannot assign to field 'amount' | Code tried to modify a frozen entry. | Create a new Debit (or post a correcting transaction). |
TypeError: unhashable type: 'Debit' | __eq__ was defined without __hash__ (or the dataclass is mutable). | Define __hash__ consistently, or use frozen=True. |
decimal.InvalidOperation | A Decimal was built from text that is not a number, such as Decimal("12,50"). | Validate and clean input; use a dot as the decimal separator. |
AttributeError: 'Ledger' object has no attribute 'total_debits' | The method is missing from the class (the original playground had exactly this bug). | Add the method, or call one that exists. |
AssertionError | An assert or test expectation failed. | Print the two values and decide which is wrong. |
Try It in Python
Experiment with the ledger model in a real Python runtime. The editor comes pre-loaded with the complete, working program from section 04. Modify the transactions, add accounts, or break the balance β and watch what happens.
Challenges: (1) add a Ledger.trial_balance(); (2) make Debit support __sub__; (3) add a currency field and reject mixed-currency additions; (4) use singledispatch so Debit.__add__ can accept a plain Decimal. Full solutions with line-by-line explanations are in section 09.
π Understand the starter code
The editor starts with the same program you studied in section 04 (“The Ledger Class”), so you already have a line-by-line explanation of every line — open it again with Step mode if you need a refresher. Compared with the original draft of this page the starter now includes total_debits() and total_credits(); without them its last print lines crashed with an AttributeError.
Experiments — predict first, then run:
- Post a fourth transaction,
Transaction("Rent", "Rent Expense", "Cash", Decimal("500")). What is the new Cash balance? (Answer:874.50.) - Replace one
Decimal("...")amount with a float such as250.10. Which line shows the damage? - Append
ledger._entries.append(Debit("Cash", Decimal("10")))before the report and run again. What doesIs balanced?say, and by how much is the total off? - Try
print(Debit("Cash", Decimal(10)) + Debit("AR", Decimal(5))). Read the error. - Write
sum(ledger._entries)and read theTypeError. Why does it name'int'first?
Extend the Ledger
The playground callout suggested four extensions. Try each one yourself first, in the playground above, and open the solution only to compare. Every solution is a complete program with a full line-by-line explanation.
Challenge 1 Β· Trial balance
Add Ledger.trial_balance() that returns a dictionary of account → balance. Hint: a dict comprehension, sum with a same-account seed, and sorted for a stable order.
Show the solution
Challenge 2 Β· Subtraction
Make a - b work. Hint: you already have negation and addition.
Show the solution
Challenge 3 Β· Currencies
Add a currency field and reject mixed-currency additions. Hint: a field with a default value, and remember to carry the currency into every new object.
Show the solution
Challenge 4 Β· Dispatch on type
Use functools.singledispatchmethod so Debit.__add__ accepts a Debit or a plain Decimal. Hint: register the handlers after the class exists.
Show the solution
β Prove it with automated tests
Instead of eyeballing printed output, encode each rule as a test. Seven small tests pin down everything the dunder methods promise — if you change Debit and break a rule, the failing test’s name tells you which.
Habit: whenever you add an operator, add one test for the normal case, one for each error case, and one for the algebra (x + (-x) is zero, -(-x) == x).
Quick Knowledge Check
Twelve questions on the ledger, its operators, Decimal and the bug hunt. Instant feedback after each answer.
Great effort β review any missed questions above.
Wrapping Up
π What You've Learned
You met operator overloading in a real domain: a double-entry ledger where every entry is a Debit and every credit is a Debit negated. Because __add__ and __neg__ are implemented, a single expression balances the books β no helper methods, no special counters.
The same technique scales: add a currency field and the pattern becomes a full accounting system. Add __lt__ and you can sort entries by amount. Add __hash__ and entries become set- and dict-friendly.
Key Takeaways
- Operator overloading is polymorphism.
sum(entries)doesn't know or care what class it's holding β it just calls+. __neg__is the whole trick. A credit is literally a debit negated. The same amount appears with opposite signs, so the total is zero.- Return
NotImplemented, don't raise. Let Python decide whether an operation is valid across the types involved. - Keep
__eq__and__hash__consistent. Two equal entries must hash the same, or sets and dicts misbehave. - Use
Decimalfor money. Float arithmetic loses cents.Decimalkeeps the books exact. - Balanced is not the same as correct. The invariant catches one-sided entries and slips, not wrong-account or missing transactions.
- Read the first failing step. The
__start__bug was found by asking which addition raised first, not by guessing. - Operators have rules; honour them everywhere. Built-ins such as
sumobey your__add__, including its same-account rule. - Invariants are stronger than checks.
sum(entries) == 0is true by construction, not by a runtime assertion β the data structure can't produce a wrong answer.
π§ Where to go next: three build projects
Bank statement viewer
Read a CSV of transactions, post each as a Transaction, and print a per-account statement with running balances.
Multi-currency ledger
Combine Challenge 3 with exchange rates. Convert entries to a base currency with a method, and keep + strict for mixed currencies.
Audit trail
Make the ledger append-only: reversing entries instead of edits, a timestamp and author on each transaction, and a test that proves balance after every post.
π Frequently asked questions
Why is Credit = Debit not a subclass?
A subclass would let isinstance(x, Credit) work, but it would also let a Credit carry a positive amount, breaking the rule that credits are negative. A single class with a sign cannot be in an inconsistent state. The cost is that you test entry.amount < 0 instead of the type.
Why does Debit define __eq__ and __hash__ when a frozen dataclass would generate them?
For teaching: writing them by hand shows the rule that equality and hashing must agree. In production code you would normally delete both and rely on @dataclass(frozen=True), which generates a consistent pair (the test program does exactly that).
Should a real ledger store a signed amount?
Some do, some store separate debit and credit columns, and many store integer minor units (cents). The best choice depends on reporting needs. Signed amounts make the arithmetic beautifully simple, which is why they suit this lesson; a presentation layer decides how to display them.
Can I use float if I round at the end?
You can, and many programs do, but errors can accumulate and equality checks like balance == 0 become unreliable. Decimal or integer cents let you keep the exact check that makes this design elegant.
Why does sum(entries) fail without start=?
sum begins at the integer 0, so its first step is 0 + entry. int does not know about Debit, and Debit has no __radd__, so Python raises TypeError. Supplying start= with a matching object, or summing numbers, avoids it.
Is operator overloading “too magical”?
It is a tool with a cost: readers must know what + does for your type. It pays off when the operator matches a real mathematical meaning (adding money, negating a debit). Avoid it when a named method like post() would be clearer.
π Glossary
A named bucket of money, such as Cash.
The two sides of an entry. Here: positive and negative amounts.
One recorded transaction with a debit side and a credit side.
The complete set of accounts and entries.
A list of all account balances whose total must be zero.
A drawing with debits on the left and credits on the right.
A condition that must always hold, here: total of entries = 0.
A method with double underscores that Python calls for operators and built-ins.
Giving your class its own meaning for an operator such as +.
A return value meaning “I cannot handle this pair of types.”
The right-hand operand’s method (__radd__) tried after the left one declines.
A class whose boilerplate methods are generated from its fields.
Read-only after creation.
Exact decimal arithmetic for money.
start)The initial value for sum().
A number used by sets and dicts to find equal objects quickly.
π Further reading
- Python data model: special method names — the official reference for every dunder method.
- dataclasses — fields,
frozen,eq, ordering. - decimal — exact arithmetic, rounding and contexts.
- functools.singledispatchmethod — dispatch on argument type.
- unittest — the standard test framework used in the tests program.
β¨ The accounting insight: debits and credits aren't two types β they're two signs of the same number. Operator overloading lets your code reflect that truth directly.
β Glenn Junsay Pansensoy, Poetic Bytes