NPV and IRR in Python: Is the Project Worth It?
- net present value
- internal rate of return
- time value of money
- discount rate
- hurdle rate
- bisection
- capital budgeting
- enumerate
NPV and IRR - Is the Project Worth It?
Scene: Thursday morning, the capex (capital expenditure) committee. Operations wants €120,000 for an automated packing line. Their slide promises savings of €30,000, €32,000, €34,000, €30,000 and €20,000 over the next five years: €146,000 back for €120,000 out, "a €26,000 profit". The CFO turns to you: "Our cost of capital is 10%. Is it worth it? NPV and IRR before lunch, please." That €26,000 is the trap. It adds euros that arrive five years from now to euros spent today, as if they were the same money. They are not.
What it is: Net present value (NPV) is the value a project adds today: every future cash flow discounted back to year 0 at the return investors require, minus the upfront cost. Internal rate of return (IRR) is the discount rate at which that NPV is exactly zero: the project's own break-even return.
Discounting: from future euros to today's euros
A euro received in t years is worth less than a euro today, because today's euro could be invested at rate r in the meantime. That is the time value of money. To bring a future cash flow back to today, divide it by (1 + r) once for every year it waits:
PV of one cash flow = CFₜ / (1 + r)ᵗ
NPV = CF₀ + CF₁ / (1 + r) + CF₂ / (1 + r)² + ... + CFₙ / (1 + r)ⁿ
Year 0 is today: (1 + r)⁰ = 1, so the purchase price enters at face value. At 10%, the €20,000 saved in year 5 is worth 20,000 / 1.1⁵ ≈ €12,418 today: more than a third of it evaporates just by waiting.
In Python the year comes free with enumerate, which hands you a counter alongside each item, starting at 0:
for t, cf in enumerate([-120000, 30000, 32000]):
print(t, cf) # prints 0 -120000, then 1 30000, then 2 32000
That counter t is exactly the exponent the formula needs.
🧠 The mental model: NPV rolls downhill as the rate rises
Picture the NPV as a ball on a slope. At a 0% rate nothing is discounted, so NPV is just the plain sum of the cash flows: operations' €26,000. As the rate climbs, every future euro is divided by a bigger number and the NPV rolls downhill. The rate where it crosses zero is the IRR. Your hurdle rate is a flag planted on that slope: flag left of the crossing, NPV is positive, accept; flag right of it, NPV is negative, reject.
| Discount rate | Future cash is | NPV | Verdict |
|---|---|---|---|
| 0% | counted at face value | the plain sum | not a decision rate |
| below the IRR | discounted, but still enough | positive | accept |
| exactly the IRR | discounted to break-even | zero | indifferent |
| above the IRR | discounted too hard | negative | reject |
Finding the IRR by bisection
Once a project runs more than a couple of years, no practical formula solves the NPV equation for r, so you search for it. Bisection is the simplest search that cannot miss, provided NPV changes sign inside the bracket. It is the same trick as guessing a number between 1 and 100 by always halving the range:
- Start with a bracket: 0% (NPV positive) and 100% (NPV negative), so the IRR must sit somewhere in between.
- Try the midpoint. If NPV is still positive there, the IRR is higher: move the lower end up to the midpoint. Otherwise move the upper end down.
- Repeat until the bracket is narrower than a tolerance.
Each step halves the bracket, so 24 halvings shrink the 0% to 100% bracket below 0.00001% (a width of 1e-7). The irr() function in the starter does exactly this and is written for you. Note what it calls on every step: your npv(). The search can only be as right as the function it searches.
The decision rule
For a conventional project (cash out first, cash in afterwards) the two rules always agree:
- NPV > 0 at the hurdle rate, equivalently IRR > hurdle: accept. The project beats what investors could earn elsewhere for the same risk.
- NPV < 0 at the hurdle rate, equivalently IRR < hurdle: reject, however large the undiscounted profit looks.
When they do disagree (ranking projects of very different sizes, or cash flows that change sign more than once), corporate-finance textbooks side with NPV, because it measures the value created in euros rather than as a percentage.
Your Task
The starter holds the cash flows, a 10% HURDLE and an npv() whose loop adds every cash flow at face value: the rate is never used. Run it first. Every NPV row prints 26,000.00, the bisection never sees a negative NPV and runs into the top of its bracket (an IRR of 100.00%), and the committee is told to ACCEPT.
Fix the # 👉 YOUR TURN line so each cash flow is divided by (1 + rate) ** t before it is added.
Predict first: the 0% row cannot move, since nothing is discounted at 0%. But at the 10% hurdle, does the €26,000 "profit" survive discounting or turn negative? And does the IRR land above or below 10%? Commit to your two guesses, then run and read the decision line.
Related terms in the glossary