How Do You Build a PF Contribution Calculator for Indian Payroll in Python?
If you run engineering payroll in India, you need a deterministic way to compute Provident Fund (PF) contributions. The short answer: employer PF is 12% of (Basic + DA), capped at ₹1,800/month (because the statutory wage ceiling is ₹15,000), and under the India Labour Codes 2025-26, Basic + DA must be at least 50% of CTC. Everything else — EPS split, admin charges, the employee share — falls out of that rule.
Below is the exact calculator I use at Versatile Club, in under 40 lines of Python. It's compliant with the 2025-26 wage code, handles the ₹15,000 ceiling correctly, and splits employer contributions between EPF and EPS the way EPFO actually does it.
What are the PF rules you have to encode?
Before writing a line of code, pin down the rules. This is where most in-house payroll scripts drift out of compliance.
| Component | Rate | Base | Cap |
|---|---|---|---|
| Employee PF | 12% | Basic + DA | None (can exceed ₹15k if voluntary) |
| Employer EPF | 3.67% | Basic + DA | ₹15,000/month statutory |
| Employer EPS | 8.33% | Basic + DA | ₹15,000/month statutory |
| EDLI | 0.50% | Basic + DA | ₹15,000/month |
| Admin charges | 0.50% | Basic + DA | ₹15,000/month (min ₹500) |
Two things trip people up. First, the employer 12% is split into 8.33% EPS and 3.67% EPF — and EPS is hard-capped at 8.33% of ₹15,000 = ₹1,250. Second, the Labour Code 2025-26 requires Basic + DA ≥ 50% of CTC, so you can't suppress PF by structuring most of the salary as allowances anymore.
What does the calculator look like in Python?
Here's a minimal, auditable version. You can drop this into any Django or FastAPI payroll service.
from dataclasses import dataclass
PF_WAGE_CEILING = 15_000 # INR per month
EMPLOYEE_PF_RATE = 0.12
EPS_RATE = 0.0833
EPF_EMPLOYER_RATE = 0.0367
EDLI_RATE = 0.005
ADMIN_RATE = 0.005
MIN_BASIC_DA_RATIO = 0.50 # Labour Code 2025-26
@dataclass
class PFBreakdown:
basic_da: float
employee_pf: float
employer_eps: float
employer_epf: float
edli: float
admin: float
total_employer: float
def compute_pf(ctc_monthly: float, basic_da: float) -> PFBreakdown:
if basic_da < MIN_BASIC_DA_RATIO * ctc_monthly:
raise ValueError(
f"Non-compliant: Basic+DA must be >= 50% of CTC "
f"(got {basic_da/ctc_monthly:.1%})"
)
capped = min(basic_da, PF_WAGE_CEILING)
employee_pf = round(basic_da * EMPLOYEE_PF_RATE, 2)
eps = round(capped * EPS_RATE, 2) # hard cap at 1,250
epf_er = round(basic_da * EMPLOYEE_PF_RATE - eps, 2)
edli = round(capped * EDLI_RATE, 2)
admin = max(round(capped * ADMIN_RATE, 2), 500.0)
return PFBreakdown(
basic_da=basic_da,
employee_pf=employee_pf,
employer_eps=eps,
employer_epf=epf_er,
edli=edli,
admin=admin,
total_employer=eps + epf_er + edli + admin,
)
Run it against a senior backend engineer at ₹35 LPA (≈ $41,900 USD/year at ₹83.5/USD):
pf = compute_pf(ctc_monthly=291_667, basic_da=145_833)
# employee_pf = 17,499.96
# employer_eps = 1,250.00 <- capped
# employer_epf = 16,249.96
# edli = 75.00
# admin = 500.00 <- floor
# total_employer = 18,074.96
Notice EPS is exactly ₹1,250 — because 8.33% applies to the statutory ceiling, not actual Basic. If your in-house script computes EPS on full Basic, you're overpaying EPFO (and under-funding the employee's EPF account).
How do you plug this into a fully-loaded cost model?
PF is only one line item. The fully-loaded multiplier we use at Versatile Club for India is CTC × 1.2081, which accounts for PF employer share, ESI (if applicable), gratuity accrual at 4.81% of Basic+DA, EDLI, admin charges, and professional tax. For our ₹35 LPA engineer, that's a true cost of ₹42.28 LPA (~$50,640 USD/year) — still dramatically cheaper than an equivalent US hire, but the 20.81% delta matters when you're modelling runway.
The open-source calculator we maintain — github.com/versatileclub/india-employment-toolkit — wraps this function plus ESI, gratuity, TDS, and Labour Code validators into a single FullyLoadedCost class. PRs welcome; we update it every quarter as EPFO circulars drop.
What should you validate in CI?
Three assertions catch 90% of payroll bugs before they hit production:
def test_basic_da_compliance():
with pytest.raises(ValueError):
compute_pf(ctc_monthly=100_000, basic_da=40_000) # 40% < 50%
def test_eps_cap():
pf = compute_pf(ctc_monthly=500_000, basic_da=250_000)
assert pf.employer_eps == 1_250.00 # hard-capped
def test_admin_floor():
pf = compute_pf(ctc_monthly=20_000, basic_da=10_000)
assert pf.admin == 500.00 # minimum admin charge
If you're running payroll for more than a handful of India employees, wire these into your CI the same way you'd test a billing system. Regulatory drift is real — we saw three EPFO circulars in 2025 alone.
Need help hiring developers in India? Versatile Club handles payroll, compliance, and onboarding. Learn more at versatile.club.
