Python code: compare FIFO, LIFO and weighted average on the same data

Python code: compare FIFO, LIFO and weighted average on the same data
Ask three people in the same business what one specific sale cost and you may get three different answers without anyone being wrong. The quarrel is not about arithmetic; it is about which purchase gets charged when goods leave the shelf. If 90 bags of cement went out on 5 March, did they come from the 100 bags bought at 18 000 on 2 March, or from the 60 bags bought at 20 000 on 9 March, which had not arrived yet on that date? Both answers exist in accounting: FIFO says the earliest purchase left first, LIFO says the most recent one did, and weighted average spreads the cost across everything on hand.
This program runs all three methods over exactly the same ten movements and the same 360 units sold. It prints a single table with the cost of sales for each method, the closing stock in units and in value, the gross profit, and how far each method sits from the average. It is written for the small-business owner who wants to know why the accountant's report does not match the mental math; for the accountant who has to show the effect of a change of method without rebuilding a spreadsheet from scratch; and for the warehouse clerk or bookkeeping assistant who checks invoices against the stock ledger kept by the system and suspects something is off.
You do not need to know how to program. Unzip the folder, open the terminal and run one command. The program uses only the Python standard library, so there is nothing else to install, and it behaves the same on Windows, Linux and Mac.
⬇ Descargar el código (ZIP)| What the ZIP holds | What it is for |
|---|---|
| comparar_metodos.py | The program, commented line by line |
| datos/movimientos.csv | Ten sample movements: purchases and sales of two products |
| datos/precios.csv | Sale price of each product, needed for the gross profit |
| salida_ejemplo.txt | The output you should get when you run it, for comparison |
| README.md | The package guide and the detail of every file |
| Python 3.11 or newer | The only requirement: the language itself |
What the program does
- Reads
datos/movimientos.csv, with the columns product, date, type, detail, quantity and unit_cost. - Reads
datos/precios.csv(product and sale_price): it only needs it to compute the gross profit of each method on the same net sales. - Runs FIFO: every purchase opens a lot with its quantity and unit cost, and every sale takes the oldest lot.
- Runs LIFO: the same sale, but taking the newest lot.
- Runs the weighted average: it recomputes the average cost after every purchase and values each sale at the average in force at that moment.
- Adds up the three results, gets the gross profit and the gap of each method against the average.
- Prints the comparison table, the closing balance per method and a conclusion, and saves the same table to
salida/comparacion.csvfor Excel.
How to run it
The only thing you need installed is Python 3.11 or newer. If you are not sure about the version, type this in the terminal:
python --version
Then unzip the package into a folder you can find easily, open the terminal and move into that folder. On Windows you can type:
cd C:\path\to\the\unzipped\folder
And the program runs with a single line:
python comparar_metodos.py
That is all. There is no virtual environment to create, no pandas to install and no internet connection needed: the program relies only on csv, decimal and pathlib, which ship with Python. If the movements file is where it should be, the table shows up in under a second and the CSV is saved in the salida folder.
The code, explained
The whole program fits in about two hundred lines. These are the pieces that explain why the same business ends up with three different costs.
1. What it does and with what. The header says it plainly:
# ==========================================================================
# FIFO · LIFO · WEIGHTED AVERAGE
# Didactic Python code for inventory · Kardex Tauro · kardex-tauro.muisca.co
# What it does: values the SAME issues with FIFO, LIFO and weighted average,
# compares the three results and saves them to salida/comparacion.csv
# Tested with Python 3.11. Standard library only: nothing to install.
# ==========================================================================
Look at the last line: standard library only. That is why you download it, run it and it works, with no fights over installs, versions or permissions.
2. Every purchase creates a lot. Here is the heart of the matter. The program does not keep a single cost per product: it keeps a list of lots, and each lot is a bag with its quantity and its unit cost.
def por_lotes(movimientos: list[dict], ultimo_primero: bool) -> dict:
"""Lot engine: a sale takes the oldest lot (FIFO) or the newest one (LIFO)."""
estado = {}
for movimiento in movimientos:
producto = movimiento["product"]
cuenta = estado.setdefault(producto, {"lotes": [], "cantidad": CERO, "valor": CERO,
"vendidas": CERO, "costo_ventas": CERO})
cantidad = Decimal(movimiento["quantity"])
if movimiento["type"] == TIPO_ENTRADA:
# # Every purchase creates a lot with its quantity and unit cost
costo = Decimal(movimiento["unit_cost"])
cuenta["lotes"].append({"cantidad": cantidad, "costo": costo})
cuenta["cantidad"] += cantidad
cuenta["valor"] += redondear(cantidad * costo)
That list of lots is the memory that lets the cost of a sale depend on which purchase is charged. Without lots there is no FIFO and no LIFO: there would be one cost and one single answer.
3. A sale eats lots, and that is where everything is decided. When a sale arrives, the program keeps taking lots until the requested quantity is issued. One sale may need part of a lot and part of another, which is why there is a while loop: the sale does not stop because a lot runs out, it moves on to the next one.
elif movimiento["type"] == TIPO_SALIDA:
# # One sale may use up SEVERAL lots, with different costs
indice = -1 if ultimo_primero else 0
pendiente, costo_total = cantidad, CERO
while pendiente > 0:
lote = cuenta["lotes"][indice]
toma = min(pendiente, lote["cantidad"])
costo_total += redondear(toma * lote["costo"])
lote["cantidad"] -= toma
pendiente -= toma
if lote["cantidad"] == 0:
cuenta["lotes"].pop(indice)
cuenta["cantidad"] -= cantidad
cuenta["valor"] -= costo_total
Read the first line of that piece slowly. The entire difference between FIFO and LIFO lives there: in the index of the lot that gets taken. With index zero it takes the oldest lot, that is FIFO. With index minus one it takes the last one in, that is LIFO. Everything else is identical. That is why both methods sell the same 360 units and only the cost moves: what changes is where the goods came from, not how many left.
4. Two names for the same engine. The public FIFO and LIFO functions are almost a joke: they call the same lot engine with a switch set to false or true.
def peps(movimientos: list[dict]) -> dict:
"""FIFO method: first in, first out."""
# # FIFO: a sale uses up the OLDEST lot first (queue)
return por_lotes(movimientos, False)
def ueps(movimientos: list[dict]) -> dict:
"""LIFO method: last in, first out."""
# # LIFO: a sale uses up the NEWEST lot first (stack)
return por_lotes(movimientos, True)
This is what many people miss when they argue about inventory methods: they are not two different programs, they are the same mechanics with a different rule about which lot goes first.
5. The weighted average does not use lots. It uses one single drawer per product: it adds quantities and values, and values each sale at the average in force before taking it out.
elif movimiento["type"] == TIPO_SALIDA:
# Cada venta se valora al promedio vigente ANTES de descontarla.
vigente = redondear(cuenta["valor"] / cuenta["cantidad"]) if cuenta["cantidad"] else CERO
costo = redondear(cantidad * vigente)
cuenta["cantidad"] -= cantidad
cuenta["valor"] -= costo
cuenta["vendidas"] += cantidad
cuenta["costo_ventas"] += costo
That word before matters: if the program took the goods out first and averaged afterwards, every sale would change the very cost it is valued at. There is a second consequence worth flagging: this is a moving average, recomputed on every purchase. If your accountant uses a monthly or periodic average, the numbers will not match to the last cent, and that does not mean one of them is wrong: they are two different ways of averaging.
6. The conclusion, with no hand-typed figures. The program does not print a fixed text: it looks for the priciest and the cheapest stock among the results and shows the gap.
# # With rising costs FIFO leaves the priciest stock and LIFO the cheapest; the average splits it.
# # Which one you may use is NOT up to the program: local accounting rules decide.
caro = max(filas, key=lambda fila: fila["inv_val"])
barato = min(filas, key=lambda fila: fila["inv_val"])
diferencia = redondear(caro["inv_val"] - barato["inv_val"])
The code comment explains it better than any manual, and it is worth repeating: with rising costs FIFO leaves the stock at the newest cost, the priciest one, and the lowest cost of sales; LIFO does the opposite; the average lands in between. And it closes with the most honest warning in the whole program: which method you may use is not up to the program, your country's accounting rules and your own internal policy decide.
What you will see on screen
With the sample data (two products, ten movements, 360 units sold and 190 still in the warehouse) the table comes out like this:
| Method | Cost of sales | Closing stock (value) | Gross profit | Difference vs average |
|---|---|---|---|---|
| FIFO | 3,240,000.00 | 1,042,500.00 | 1,080,000.00 | -36,397.70 |
| LIFO | 3,312,000.00 | 970,500.00 | 1,008,000.00 | 35,602.30 |
| Weighted average | 3,276,397.70 | 1,006,102.30 | 1,043,602.30 | 0.00 |
The closing block on screen puts it bluntly:
CONCLUSION Priciest stock: FIFO = 1,042,500.00 Cheapest stock: LIFO = 970,500.00 Difference (a matter of method only, not money lost): 72,000.00
It is worth stopping on that last line. Between the priciest and the cheapest stock there is a gap of 72 000, and it is neither money lost nor money earned: once those 190 units are sold, the gap reverses. What the method decides is which month shows the cost, not what the goods cost overall. That is why it pays to look at profit over a long period instead of a single month when you compare one method against another. The very same result lands in salida/comparacion.csv in plain format, ready for a spreadsheet: 3240000.00 as the FIFO cost of sales, 3312000.00 for LIFO and 3276397.70 for the weighted average, with closing stock of 1042500.00 and 970500.00 and a difference of 72000.00.
Common mistakes and tips
- Watch out for mistyped product names. The program separates balances by product name. If one line says Gray cement 50 kg and another says gray cement 50 kg, the program sees two products and the balances end up split.
- Sales go in with no unit cost. In the sample file that column is left empty on sales and it should stay that way: the program works out the cost from the method being tested.
- If a sale is bigger than the balance, the program stops with an error. That is on purpose: it means a purchase is missing or a sale was entered twice. Check the file, do not paper over it.
- Load the movements in date order. All three methods depend on the order: FIFO and LIFO because they take lots one way or the other, and the average because it is recomputed on the fly. An unsorted file gives a result that means nothing.
- Cents in the average are normal. A weighted average rarely lands on round figures, so the program works with cents and carries them along. If you compare against a system that rounds to whole units, you will find small differences.
- Do not compare different methods across different months. The effect of the method only shows on the same movements: if you change the method and the period at the same time, you will never know what caused the gap.
- Keep the CSV of every run. The comparison table is the evidence behind your decision. If someone later asks why one method is used and not another, that file answers.
When this is not enough
This program is a desk lab: it reads a CSV, computes and stops. It does not identify users, does not log who edited a movement, does not handle several warehouses, does not match purchase orders against invoices and leaves no trail for an audit. When the inventory no longer fits into a spreadsheet plus a script —because there are several warehouse clerks, hundreds of items and movements every day— the sensible step is a stock program that keeps the ledger on its own. Kardex Tauro is a free program that does exactly that: it orders the inventory and keeps the balances without you touching data files. This code is for the step before: understanding the logic, testing the effect of the three methods and knowing what you are looking at when your system shows a cost.
⬇ Descargar el código (ZIP)Download the ZIP, run it with the sample data and then swap the file for your own movements. Once you see the three costs side by side you will understand, in one go, why the inventory method is an accounting decision and not a simple calculation: the goods are the same, the units sold are the same, and the profit for the month still changes.