A Python package for financial modeling and reporting
Project description
pyproforma
A Python library for building financial models — a code-first alternative to Excel for pro formas, projections, and structured financial tables.
pip install pyproforma
Why
Spreadsheets are the default tool for financial modeling, but they have real problems: no version control, no testing, formulas hidden inside cells, and no easy way to generate the same model for multiple scenarios. pyproforma is designed for analysts who want the benefits of code — reproducibility, testability, version history — without giving up the tabular output that finance people actually use.
How it works
Define a model by subclassing ProformaModel and declaring line items as class attributes. Instantiate it with a list of periods and the library calculates everything.
from pyproforma import ProformaModel, FixedLine, FormulaLine, ScalarLine, Format
class IncomeStatement(ProformaModel):
default_periods = [2024, 2025, 2026]
tax_rate = ScalarLine(value=0.21, label="Tax Rate")
revenue = FixedLine(
values={2024: 500_000, 2025: 550_000, 2026: 605_000},
label="Revenue",
tags=["operating"],
value_format=Format.CURRENCY_NO_DECIMALS,
)
cogs = FormulaLine(
formula=lambda li, t: li.revenue[t] * 0.55,
label="Cost of Goods Sold",
value_format=Format.CURRENCY_NO_DECIMALS,
)
gross_profit = FormulaLine(
formula=lambda li, t: li.revenue[t] - li.cogs[t],
label="Gross Profit",
value_format=Format.CURRENCY_NO_DECIMALS,
)
net_income = FormulaLine(
formula=lambda li, t: li.gross_profit[t] * (1 - li.tax_rate),
label="Net Income",
value_format=Format.CURRENCY_NO_DECIMALS,
)
model = IncomeStatement() # uses default_periods
Access results with dot notation (primary) or bracket notation (useful when the name is in a variable):
model.net_income[2025] # 173_745.0 — dot notation
model["net_income"][2025] # same value — bracket notation
model.tax_rate.value # 0.21 — scalars have .value, not [t]
model.periods # [2024, 2025, 2026]
model.line_item_names # ["revenue", "cogs", "gross_profit", "net_income"]
model.scalar_names # ["tax_rate"]
Line item types
| Type | Use |
|---|---|
FixedLine(values, ...) |
Hardcoded values per period |
FormulaLine(formula, ...) |
Calculated from other items via a lambda |
ScalarLine(value, ...) |
A single value shared across all periods |
InputLine(default, ...) |
Period-indexed values supplied at instantiation |
ScalarInputLine(default, ...) |
A single value supplied at instantiation |
Formula lambdas receive li (the model namespace) and t (the current period). Period-indexed items use li.name[t]; scalars use li.name (no [t]). Reference prior periods with li.name[t-1].
Scenario inputs
InputLine and ScalarInputLine let callers supply values at instantiation without subclassing again — useful for scenario analysis.
from pyproforma import InputLine, ScalarInputLine
class FlexModel(ProformaModel):
default_periods = [2024, 2025, 2026]
margin = ScalarInputLine(default=0.45, label="Gross Margin")
revenue = FixedLine(
values={2024: 500_000, 2025: 550_000, 2026: 605_000},
label="Revenue",
value_format=Format.CURRENCY_NO_DECIMALS,
)
gross_profit = FormulaLine(
formula=lambda li, t: li.revenue[t] * li.margin,
label="Gross Profit",
value_format=Format.CURRENCY_NO_DECIMALS,
)
base = FlexModel() # uses default margin of 0.45
upside = FlexModel(margin=0.52) # override at instantiation
Use model.compare() to diff two instances:
comparison = base.compare(upside, labels=["Base", "Upside"])
Time-series formulas
Seed a value in the first period and let the formula compound from there — no if t == first_year guards needed:
revenue = FormulaLine(
formula=lambda li, t: li.revenue[t-1] * (1 + li.growth_rate),
values={2024: 500_000}, # engine uses this for 2024; formula runs from 2025 onward
label="Revenue",
)
Tags
Tag line items to group them without fixed categories:
water_sales = FixedLine(values={...}, tags=["revenue"])
power_sales = FixedLine(values={...}, tags=["revenue"])
# Sum all "revenue"-tagged items in a formula
total_revenue = FormulaLine(formula=lambda li, t: li.tag["revenue"][t])
Tags also work in table templates:
from pyproforma import TagTotalRow
TagTotalRow(tag="revenue", label="Total Revenue")
Tables
Generate formatted tables for HTML, Excel, or pandas. from_template gives full control over layout:
from pyproforma import HeaderRow, LabelRow, ItemRow, BlankRow, LineItemsTotalRow
table = model.tables.from_template([
HeaderRow(),
LabelRow("Income Statement"),
ItemRow("revenue"),
ItemRow("cogs", reverse_sign=True), # display as positive deduction
ItemRow("gross_profit", bold=True, borders="top"),
BlankRow(),
ItemRow("net_income", bold=True),
])
table.show() # inline in Jupyter
table.to_excel("output.xlsx") # Excel with formatting preserved
table.to_dataframe() # pandas DataFrame
Convenience builders for common layouts:
model.tables.line_items().show() # all line items
model.tables.line_item("net_income", include_percent_change=True).show()
model.tables.precedents("net_income").show() # formula dependency tree
Charts
model.charts.line_item("net_income", chart_type="bar").show()
model.charts.line_items(["revenue", "gross_profit", "net_income"]).show()
Charts return a ChartSpec which can also render to a matplotlib Figure:
fig = model.charts.line_item("net_income").figure()
Requires pip install pyproforma[charts].
Number formatting
Named format constants flow through to both HTML and Excel output:
from pyproforma import Format
ItemRow("revenue", value_format=Format.CURRENCY_NO_DECIMALS) # $500,000
ItemRow("revenue", value_format=Format.THOUSANDS_K) # 500.0K
ItemRow("margin", value_format=Format.PERCENT_ONE_DECIMAL) # 45.0%
ItemRow("net_income", value_format=Format.MILLIONS_M) # $0.2M
Custom formats via NumberFormatSpec:
from pyproforma import NumberFormatSpec
fmt = NumberFormatSpec(decimals=1, scale="millions", prefix="$", suffix="M")
# 500_000 → "$0.5M"
Explorer
A lightweight Flask web app for browsing any model interactively:
from pyproforma.explorer import create_app
app = create_app(model)
app.run(debug=True)
Requires pip install pyproforma[explorer]. The app shows all line items, their values, formula sources, and lets you update InputLine / ScalarInputLine values live. You can also pass named tables, charts, and views to build a richer dashboard.
Installation
pip install pyproforma # core only
pip install pyproforma[charts] # + matplotlib
pip install pyproforma[excel] # + openpyxl
pip install pyproforma[explorer] # + Flask
pip install pyproforma[pandas] # + pandas
Requires Python 3.9+.
Status
Active development. Core modeling, table export, charts, and the Flask explorer are all stable. Feedback welcome — open an issue on GitHub.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyproforma-0.2.5.tar.gz.
File metadata
- Download URL: pyproforma-0.2.5.tar.gz
- Upload date:
- Size: 71.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0f607f0a877ef4bbb424fa223455506c034e92a831d705bbd0a3c2f96b2a2691
|
|
| MD5 |
dd848154fafa2932751834ab66663851
|
|
| BLAKE2b-256 |
5a51bf51780b17278bc537de06efecbf6592f3ddc385670b01fd38064b5190d3
|
File details
Details for the file pyproforma-0.2.5-py3-none-any.whl.
File metadata
- Download URL: pyproforma-0.2.5-py3-none-any.whl
- Upload date:
- Size: 87.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8939a766dd9ea1694bcf6bce19349f40eb01940ea297a45213d5eaa69a1fbca3
|
|
| MD5 |
c5545ebd5e0cb7894eb4ad1fd85f0049
|
|
| BLAKE2b-256 |
8a5c4232114c9356cb013826484f253618aec3c42fa12aba20a57261dfd50d68
|