A firm should invest in a project if and only if its net present value is positive. NPV is the gold standard. IRR and payback period are useful shortcuts, but each has traps.
The NPV rule
Net present value discounts all future cash flows back to today at the opportunity cost of capital, then subtracts the initial investment. If NPV > 0, the project creates value. If NPV < 0, it destroys value. Accept all positive-NPV projects.
# Net Present Valuedef npv(rate, cash_flows):
returnsum(cf / (1 + rate)**t for t, cf inenumerate(cash_flows))
project = [-1000, 400, 400, 400, 400]
for r in [0.10, 0.15, 0.20, 0.25]:
print(f"NPV at {r:.0%}: ${npv(r, project):.0f}")
print(f"At 10% cost of capital: {'ACCEPT' if npv(0.10, project) > 0 else 'REJECT'}")
Internal rate of return (IRR) and its pitfalls
The IRR is the discount rate that makes NPV exactly zero. For conventional projects (one outflow followed by inflows), IRR works fine: accept if IRR exceeds the cost of capital. But IRR breaks down with non-conventional cash flows (multiple sign changes produce multiple IRRs) and when comparing mutually exclusive projects of different scales.
Scheme
; Finding IRR by bisection; IRR is the rate r where NPV(r) = 0
(define (npv rate flows)
(define (helper fs t)
(if (null? fs) 0
(+ (/ (car fs) (expt (+ 1 rate) t))
(helper (cdr fs) (+ t 1)))))
(helper flows 0))
(define (find-irr flows lo hi iterations)
(if (= iterations 0)
(/ (+ lo hi) 2)
(let ((mid (/ (+ lo hi) 2)))
(if (> (npv mid flows) 0)
(find-irr flows mid hi (- iterations 1))
(find-irr flows lo mid (- iterations 1))))))
; Project: -1000, +400, +400, +400, +400
(define project (list -1000400400400400))
(define irr (find-irr project 0.01.050))
(display "IRR = ") (display (* 100 irr)) (display "%") (newline)
(display "NPV at IRR: $") (display (round (npv irr project))) (newline)
; Pitfall: non-conventional cash flows can have multiple IRRs; Project: -100, +230, -132 (two sign changes)
(define weird (list -100230-132))
(display "Weird project NPV at 10%: $") (display (npv 0.10 weird)) (newline)
(display "Weird project NPV at 20%: $") (display (npv 0.20 weird))
Python
# IRR by bisectiondef npv(rate, flows):
returnsum(cf / (1 + rate)**t for t, cf inenumerate(flows))
def find_irr(flows, lo=0.0, hi=1.0, iterations=50):
for _ inrange(iterations):
mid = (lo + hi) / 2if npv(mid, flows) > 0:
lo = mid
else:
hi = mid
return (lo + hi) / 2
project = [-1000, 400, 400, 400, 400]
irr = find_irr(project)
print(f"IRR = {irr:.2%}")
print(f"NPV at IRR: ${npv(irr, project):.2f}")
# Pitfall: non-conventional cash flows
weird = [-100, 230, -132]
print(f"Weird project NPV at 10%: ${npv(0.10, weird):.2f}")
print(f"Weird project NPV at 20%: ${npv(0.20, weird):.2f}")
Payback period
The payback period counts how many years it takes to recover the initial investment. Simple and intuitive, but it ignores the time value of money and everything that happens after payback. The discounted payback period fixes the first problem but not the second.
# Payback perioddef payback(cash_flows):
cumulative = cash_flows[0]
for year, cf inenumerate(cash_flows[1:], 1):
cumulative += cf
if cumulative >= 0:
return year
returnNonedef discounted_payback(rate, cash_flows):
cumulative = cash_flows[0]
for year, cf inenumerate(cash_flows[1:], 1):
cumulative += cf / (1 + rate) ** year
if cumulative >= 0:
return year
returnNoneprint(f"Project A payback: year {payback([-1000, 500, 500, 200])}")
print(f"Project B payback: year {payback([-1000, 100, 200, 300, 400, 500])}")
print(f"Project A discounted payback at 10%: year {discounted_payback(0.10, [-1000, 500, 500, 200])}")
Comparing mutually exclusive projects
When you can only pick one project, IRR can mislead. A small project with 50% IRR is not necessarily better than a large project with 20% IRR. NPV settles every comparison: pick the project with the highest NPV. When projects have different lives, use equivalent annual annuity to compare.
Scheme
; Mutually exclusive projects: NPV wins over IRR
(define (npv rate flows)
(define (helper fs t)
(if (null? fs) 0
(+ (/ (car fs) (expt (+ 1 rate) t))
(helper (cdr fs) (+ t 1)))))
(helper flows 0))
(define (find-irr flows)
(define (bisect lo hi n)
(if (= n 0) (/ (+ lo hi) 2)
(let ((mid (/ (+ lo hi) 2)))
(if (> (npv mid flows) 0)
(bisect mid hi (- n 1))
(bisect lo mid (- n 1))))))
(bisect 0.01.050))
; Small project: invest $100, get $150 next year
(define small (list -100150))
; Large project: invest $10000, get $13000 next year
(define large (list -1000013000))
(define r 0.10)
(display "Small: IRR = ") (display (round (* 100 (find-irr small)))) (display "%")
(display ", NPV = $") (display (round (npv r small))) (newline)
(display "Large: IRR = ") (display (round (* 100 (find-irr large)))) (display "%")
(display ", NPV = $") (display (round (npv r large))) (newline)
; IRR says small is better (50% > 30%); NPV says large is better ($36 > $1818) โ NPV is correct
(display "Choose: ") (display (if (> (npv r large) (npv r small)) "LARGE""SMALL"))
(display " (higher NPV)")
Python
# Mutually exclusive projects: NPV > IRR for decisionsdef npv(rate, flows):
returnsum(cf / (1 + rate)**t for t, cf inenumerate(flows))
def find_irr(flows, lo=0.0, hi=1.0):
for _ inrange(50):
mid = (lo + hi) / 2if npv(mid, flows) > 0: lo = mid
else: hi = mid
return (lo + hi) / 2
small = [-100, 150]
large = [-10000, 13000]
r = 0.10print(f"Small: IRR = {find_irr(small):.0%}, NPV = ${npv(r, small):.0f}")
print(f"Large: IRR = {find_irr(large):.0%}, NPV = ${npv(r, large):.0f}")
print(f"Choose: {'LARGE' if npv(r, large) > npv(r, small) else 'SMALL'} (higher NPV)")
Neighbors
๐ Finance Ch.1 — time value of money: the foundation NPV is built on
๐ Finance Ch.4 — risk and return: how to choose the discount rate
๐ Finance Ch.6 — CAPM provides the cost of capital for NPV calculations