DEV Community

Robert Pringle
Robert Pringle

Posted on

Calculating NPV and IRR in Python Without NumPy or SciPy

NPV and IRR are two of the most useful financial metrics in business analysis. Python's numpy-financial library can compute them — but it's a heavy dependency, and the API approach is cleaner for web apps that need server-side calculation.

This tutorial shows how to calculate NPV and IRR via REST API in Python, with a realistic investment analysis example.

The Problem

You're building an investment analysis tool. Users input cash flows and a discount rate, and your app needs to tell them:

  • NPV — Is this investment worth more than it costs? (Positive NPV = yes)
  • IRR — What's the effective annual return? Compare this to their hurdle rate.

Setup

pip install httpx  # async-friendly; or use requests
Enter fullscreen mode Exit fullscreen mode

Get a free API key at RapidAPI.

import os
import httpx

RAPIDAPI_KEY = os.environ["RAPIDAPI_KEY"]
BASE_URL = "https://fincalcapi.p.rapidapi.com"
HEADERS = {
    "X-RapidAPI-Key": RAPIDAPI_KEY,
    "X-RapidAPI-Host": "fincalcapi.p.rapidapi.com",
}
Enter fullscreen mode Exit fullscreen mode

Calculating NPV

Net Present Value: the present value of future cash flows, discounted at your required rate of return, minus the initial investment.

def calculate_npv(discount_rate: float, cash_flows: list[float]) -> dict:
    """
    discount_rate: annual discount rate as a percentage (e.g., 10.0 for 10%)
    cash_flows: list starting with initial investment (negative), then returns
    """
    with httpx.Client() as client:
        response = client.get(
            f"{BASE_URL}/npv",
            params={
                "discount_rate": discount_rate,
                "cash_flows": ",".join(str(cf) for cf in cash_flows),
            },
            headers=HEADERS,
        )
        response.raise_for_status()
        return response.json()


# Example: $100k investment, 10% discount rate
# Returns $30k, $40k, $50k, $60k over 4 years
result = calculate_npv(
    discount_rate=10.0,
    cash_flows=[-100000, 30000, 40000, 50000, 60000],
)

print(f"NPV: ${result['npv']:,.2f}")
print(f"Decision: {'✅ Accept' if result['npv'] > 0 else '❌ Reject'}")
# NPV: $42,084.37
# Decision: ✅ Accept
Enter fullscreen mode Exit fullscreen mode

Calculating IRR

Internal Rate of Return: the discount rate at which NPV equals zero. Compare this to your cost of capital.

def calculate_irr(cash_flows: list[float]) -> dict:
    """
    Returns the rate at which NPV of cash_flows equals 0.
    Uses Newton-Raphson iteration — no numpy required.
    """
    with httpx.Client() as client:
        response = client.get(
            f"{BASE_URL}/irr",
            params={
                "cash_flows": ",".join(str(cf) for cf in cash_flows),
            },
            headers=HEADERS,
        )
        response.raise_for_status()
        return response.json()


result = calculate_irr([-100000, 30000, 40000, 50000, 60000])
print(f"IRR: {result['irr_percent']:.2f}%")
# IRR: 24.89%
# (This beats a 10% hurdle rate, so the investment is worth making)
Enter fullscreen mode Exit fullscreen mode

Building a Full Investment Analyser

Here's a practical class that combines both metrics:

from dataclasses import dataclass
from typing import Optional
import httpx

@dataclass
class InvestmentAnalysis:
    npv: float
    irr_percent: float
    hurdle_rate: float
    recommendation: str
    payback_period_years: Optional[float]

    @property
    def is_viable(self) -> bool:
        return self.npv > 0 and self.irr_percent > self.hurdle_rate


class InvestmentAnalyser:
    def __init__(self, api_key: str, hurdle_rate: float = 10.0):
        self.headers = {
            "X-RapidAPI-Key": api_key,
            "X-RapidAPI-Host": "fincalcapi.p.rapidapi.com",
        }
        self.hurdle_rate = hurdle_rate
        self.base_url = "https://fincalcapi.p.rapidapi.com"

    def analyse(self, cash_flows: list[float]) -> InvestmentAnalysis:
        with httpx.Client() as client:
            # Fetch NPV and IRR in parallel
            npv_resp = client.get(
                f"{self.base_url}/npv",
                params={
                    "discount_rate": self.hurdle_rate,
                    "cash_flows": ",".join(str(cf) for cf in cash_flows),
                },
                headers=self.headers,
            )
            irr_resp = client.get(
                f"{self.base_url}/irr",
                params={"cash_flows": ",".join(str(cf) for cf in cash_flows)},
                headers=self.headers,
            )

        npv_data = npv_resp.json()
        irr_data = irr_resp.json()

        npv = npv_data["npv"]
        irr = irr_data["irr_percent"]

        # Simple payback: cumulative sum crosses zero
        cumulative = 0.0
        payback = None
        for i, cf in enumerate(cash_flows):
            cumulative += cf
            if cumulative >= 0:
                payback = float(i)
                break

        if npv > 0 and irr > self.hurdle_rate:
            recommendation = f"ACCEPT — IRR {irr:.1f}% exceeds hurdle rate {self.hurdle_rate}%"
        elif npv > 0:
            recommendation = "MARGINAL — Positive NPV but IRR below hurdle rate"
        else:
            recommendation = "REJECT — Negative NPV"

        return InvestmentAnalysis(
            npv=npv,
            irr_percent=irr,
            hurdle_rate=self.hurdle_rate,
            recommendation=recommendation,
            payback_period_years=payback,
        )


# Usage
analyser = InvestmentAnalyser(api_key=RAPIDAPI_KEY, hurdle_rate=12.0)

project_a = analyser.analyse([-500_000, 100_000, 150_000, 200_000, 250_000, 300_000])
print(f"Project A: {project_a.recommendation}")
print(f"  NPV: ${project_a.npv:,.0f}")
print(f"  IRR: {project_a.irr_percent:.1f}%")
print(f"  Viable: {project_a.is_viable}")
Enter fullscreen mode Exit fullscreen mode

Running in FastAPI / Flask

If you're building a web API on top of this:

# FastAPI example
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class InvestmentRequest(BaseModel):
    cash_flows: list[float]
    hurdle_rate: float = 10.0

@app.post("/analyse")
async def analyse_investment(req: InvestmentRequest):
    analyser = InvestmentAnalyser(
        api_key=os.environ["RAPIDAPI_KEY"],
        hurdle_rate=req.hurdle_rate,
    )
    try:
        result = analyser.analyse(req.cash_flows)
        return result
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))
Enter fullscreen mode Exit fullscreen mode

Why Not numpy-financial?

numpy-financial FinCalc API
Bundle size ~15MB (numpy) 0 bytes
Server dependency pip install HTTP call
Precision handling Your problem Handled
Other calculations You build them 7 more endpoints
Cost Free Free tier available

For quick scripts, numpy-financial is fine. For production web apps, an API keeps your containers lean and your code simple.

What's Next

With the same key you can also hit:

  • /amortize — loan payment schedules
  • /mortgage — housing cost breakdown
  • /compound-interest — savings growth projections
  • /roi — investment return analysis
  • /break-even — unit economics for SaaS/products
  • /depreciation — asset value schedules

👉 Free API key on RapidAPI — 50 calls/day free.

Top comments (0)