The Analytical Investor
Month 5, Week 3 · week 19 of 48Line height: each week’s reading time, not a price

Building a Basic Equity Valuation Model in Python

Quant Lab Month 5, Week 3 5 min read By Timothy Aine

Last week on the Analyst Desk we walked through discounted cash flow valuation conceptually. The natural next step is to make it executable, because a valuation model in code has virtues a spreadsheet struggles to match: every assumption is explicit and named, sensitivity analysis becomes a loop instead of an afternoon, and the model can be rerun across scenarios in milliseconds.

More importantly, coding a valuation forces intellectual honesty. A spreadsheet lets fragile logic hide in cell references; a script makes the reasoning linear and auditable. When your valuation is wrong (and can argue that every valuation is wrong to some degree) a coded model shows you where.

This article builds a compact but complete two-stage DCF in Python, then uses it the way professionals actually should: not to produce one number, but to map how value responds to assumptions, and to reverse-engineer what the market price implies.

A two-stage DCF assumes a business passes through two eras. In the explicit stage (years 1–N), we forecast free cash flows year by year, capturing above-average growth while it lasts. In the terminal stage, the business matures and grows at a modest perpetual rate, valued with the Gordon growth formula. Every cash flow is discounted to present value at rate r, summed, adjusted for net debt, and divided by shares outstanding.

The model’s honest structure is: value = near-term forecast + long-term assumption, with the second component usually dominating. Keeping that dominance visible is one of the main reasons to build the model yourself.

The Model: Python Implementation

import numpy as np
# ------------------------------------------------------------------
# Two-stage DCF valuation
# All monetary figures in millions; per-share output in units.
# ------------------------------------------------------------------
def dcf_value(fcf0, growth_rates, r, g_terminal,
net_debt=0.0, shares=1.0):
"""
fcf0 : most recent annual free cash flow
growth_rates : list of annual FCF growth rates for explicit years
r : discount rate (cost of equity / WACC proxy)
g_terminal : perpetual growth rate (must be < r)
net_debt : total debt minus cash
shares : shares outstanding
Returns dict with enterprise value, equity value, per-share value,
and the share of value contributed by the terminal stage.
"""
if g_terminal >= r:
raise ValueError("Terminal growth must be below discount rate.")
# --- explicit stage ---
fcf, pv_explicit = fcf0, 0.0
for t, g in enumerate(growth_rates, start=1):
fcf *= (1 + g)
pv_explicit += fcf / (1 + r) ** t
# --- terminal stage (Gordon growth on year N+1 cash flow) ---
n = len(growth_rates)
tv = fcf * (1 + g_terminal) / (r - g_terminal)
pv_terminal = tv / (1 + r) ** n
ev = pv_explicit + pv_terminal
equity = ev - net_debt
return {
"enterprise_value": ev,
"equity_value": equity,
"value_per_share": equity / shares,
"terminal_share": pv_terminal / ev,
}
# ------------------------------------------------------------------
# Base case: a maturing business
# ------------------------------------------------------------------
base = dcf_value(
fcf0=100.0,
growth_rates=[0.12, 0.10, 0.09, 0.07, 0.06, 0.05, 0.05, 0.04, 0.04, 0.03],
r=0.12,
g_terminal=0.03,
net_debt=150.0,
shares=50.0,
)
print(f"Value per share: {base['value_per_share']:.2f}")
print(f"Terminal value share of EV: {base['terminal_share']:.0%}")

With these inputs the model produces a per-share value of about 27.3, with the terminal stage contributing roughly 46% of enterprise value (and that is for a business whose growth fades to 3% within the forecast window; faster growers push the terminal share well past half).

Sensitivity analysis: the real deliverable

# ------------------------------------------------------------------
# Map value across discount-rate / terminal-growth combinations
# ------------------------------------------------------------------
header = "r \\ g"
print(f"\n{header:>8}", *[f"{g:>8.1%}" for g in (0.02, 0.03, 0.04)])
for r in (0.10, 0.12, 0.14):
row = [dcf_value(100.0,
[0.12,0.10,0.09,0.07,0.06,0.05,0.05,0.04,0.04,0.03],
r, g, 150.0, 50.0)["value_per_share"]
for g in (0.02, 0.03, 0.04)]
print(f"{r:>8.0%}", *[f"{v:>8.2f}" for v in row])

A representative output:

   r \ g     2.0%     3.0%     4.0%
     10%    33.48    36.31    40.09
     12%    25.78    27.28    29.16
     14%    20.69    21.56    22.60

Read the table like a professional: across plausible assumption ranges, “fair value” spans roughly 21 to 40 which is nearly a factor of two. Any analyst quoting this company’s value to two decimal places is reporting the middle of a wide distribution as if it were a fact. The honest conclusions available are coarser but far more useful: at a price of 15, the stock is cheap under every assumption set in the table; at 45, it is expensive under all of them; between roughly 21 and 40, the model simply cannot adjudicate.

Inverting the model: what does the price imply?

from scipy.optimize import brentq
def implied_growth(price, fcf0, r, g_terminal, net_debt, shares,
years=10):
"""Uniform explicit-stage growth rate implied by market price."""
f = lambda g: dcf_value(fcf0, [g]*years, r, g_terminal,
net_debt, shares)["value_per_share"] - price
return brentq(f, -0.5, 0.60)
for price in (15, 25, 35):
g = implied_growth(price, 100.0, 0.12, 0.03, 150.0, 50.0)
print(f"Price {price:>3}: implies {g:.1%} annual FCF growth for 10 years")

This inversion which is solving for the growth the price requires rather than forecasting the growth we expect, is often the most decision-relevant use of a DCF. Judging whether a business can plausibly grow cash flow 15% annually for a decade is a question about competitive dynamics and market size, on which an informed investor can hold a defensible view. Forecasting the same number from scratch is guesswork wearing a lab coat.

Limitations and Real-World Complexity

The model above is deliberately minimal, and its simplifications should be named. It treats free cash flow as a single input, when real analysis must construct it from revenue, margins, taxes, capital expenditure, and working capital, and interrogate its quality. It uses one discount rate throughout, though risk often declines as a business matures. It ignores dilution, stock-based compensation, cyclicality, and the possibility of failure. A two-stage model quietly assumes immortality, which for most firms is generous. And the Gordon terminal value, contributing nearly half of the answer here (and often far more), remains an assumption about the infinite future compressed into two parameters.

There is also a subtler danger specific to coded models: executable precision breeds unearned confidence. A script that runs cleanly feels more authoritative than a rough estimate, yet its output inherits every weakness of its inputs. The remedy is procedural, never report a point value without its sensitivity table, and never trust a valuation you haven’t inverted.

The Long-Term Lesson

Building a valuation model teaches a lesson that merely reading about valuation cannot: the number at the bottom is soft, and the structure around it is hard. The arithmetic of discounting is exact; the inputs are opinions. Once you have watched fair value double in response to two percentage points of assumption drift, you are permanently immunized against both the analyst who announces a precise price target and your own temptation to believe one.

What remains after the humility is a durable edge. The investor who can run this model (and especially run it backward) no longer receives market prices passively. Every quote becomes a claim about the future that can be extracted, examined, and judged. Most market participants never perform that examination. Performing it, routinely and honestly, is one of the few analytical habits that compounds as reliably as capital.

Code for this article: the exact script used above · a narrated notebook with charts · the full Analytical Investor toolkit on GitHub

More from Quant Lab

  1. W3Modeling Investor Behaviour Using Historical DrawdownsQuant Lab5 min
  2. W3Building the Efficient Frontier: A Python Guide for InvestorsQuant Lab6 min
  3. W3Estimating Beta Using Regression in PythonQuant Lab5 min

Get the articles by email, in course order. Choose how many; nothing else arrives.

New subscribers also get the Investment Growth Simulator, a free Excel workbook.

How much would you like?