ccnl-engine
A Python library for modeling Italian collective labor agreements (CCNL) as structured, versioned data and computing gross-to-net salary and employer cost from first principles.
Motivation
I never really understood employment contracts or pay slips. The whole system strikes me as needlessly complicated. On top of that, finding reliable CCNL information online feels like an uphill battle: conflicting figures are everywhere, and identifying authoritative sources is harder than it should be. This project grew out of a desire to understand a little more. I make no claim to becoming an expert, but I hope to make this information more accessible and comprehensible for everyone.
The Problem
Italian payroll is governed by collective agreements (CCNL) that define base salaries, seniority increments, and allowances as time-series values — they change at negotiated renewal dates. Existing tools either lock this data inside proprietary systems or require a full HRMS. This library treats each CCNL as a validated JSON file and the computation as a pure function:
compute(PayrollScenario) → Calculation
The returned Calculation is self-describing: along with the PayrollResult (.result) it records the engine version, the exact CCNL / tax / INPS / surtax ruleset revisions used (.ruleset_version), and a snapshot of the inputs (.input_snapshot) — so any figure can be traced back to the engine and data that produced it.
Quickstart
from datetime import date
from ccnl_engine import (
Employee, Employer, Employment,
PayrollScenario, Permanent, compute,
)
calculation = compute(PayrollScenario(
employee=Employee(level_code="4"),
employment=Employment(
ccnl="commercio-confcommercio.json",
contract=Permanent(),
employer=Employer(num_employees=50),
calculation_date=date(2026, 9, 1),
),
))
payroll = calculation.result # attributes are also forwarded onto the calculation
print(payroll.net_annual) # → Decimal('...')
print(payroll.trattamento_integrativo) # → Decimal('...') — Art. 1 D.L. 3/2020 bonus
print(payroll.fiscal_simplifications) # → frozenset of items not computed by the engine
print(payroll.employer_cost_annual) # → Decimal('...')
print(calculation.engine_version) # → '0.5.0'
print(calculation.ruleset_version) # → {'ccnl': '…', 'tax': '…', 'inps': '…', 'surtax': '…'}
CCNL coverage
Over 100 contracts covering approximately 16 million employees across private and public sectors.
- L1 — Gross: base salary, seniority, fixed allowances, additional months, hourly rate.
- L2 — Net: INPS contributions, TFR, IRPEF, regional/municipal surtax.
- L3 — Work rules: overtime and night/holiday premiums, absence deduction, leave accrual, sick-pay integration, performance bonuses, welfare/benefits.
Coverage % = (L1 × 50% + L2 × 35% + L3 × 15%) − 5% per missing data note (max −20%).
→ CCNL coverage table — per-contract coverage, verification status, and feature breakdown
Scope boundaries
| Feature | In net_annual? |
How to activate |
|---|---|---|
| L3 work rules — overtime, absence, sick pay, bonus, welfare | No — reported separately | Pass OvertimeHours, AbsenceDays, etc. to PayrollScenario |
| Family-dependent deductions (Art. 12 TUIR) | Yes | Pass FamilyComposition to PayrollScenario.family |
| Art. 15 mortgage-interest deduction | Yes | Pass Art15Deductions to PayrollScenario.art15_deductions |
| PdR flat tax on premio di risultato | Reported separately | Set BonusInput.eligible_for_pdr=True |
| Bilateral fund contributions (EST, Fon.Te, …) | — | Not modelled |
See API docs for full detail.
Disclaimer
This library is not legal or tax advice. Figures are computed from publicly available CCNL tables and statutory rates as of the dates indicated in the data files. Always verify results against official sources or a qualified payroll professional.
Metadata
Release files for ccnl-engine 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ccnl_engine-0.5.1.tar.gz | 757.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ccnl_engine-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / ccnl_engine-0.5.1.tar.gz
| Download URL | ccnl_engine-0.5.1.tar.gz |
|---|---|
| Size | 757.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8b8a34ebcb502785fcf291a6752562aa7c5cde83d26556ab2a9669c86ed116ae
|
|
BLAKE2b-256 checksum How to use checksums |
6233e1cec1159ed3d87cb573e479c7683a33ff463485111990a7c2df8b9e8a19
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency logRelease files / ccnl_engine-0.5.1-py3-none-any.whl
| Download URL | ccnl_engine-0.5.1-py3-none-any.whl |
|---|---|
| Size | 681.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b4c4cd044a7d3e7d4e9d8479a21e7d3f2b41a6a66abdc664719b71320aef4ba6
|
|
BLAKE2b-256 checksum How to use checksums |
67140e4faef4c3f4dbf800a7a8bc3d1ca82589a716de8ffc4b95e1a617a271e3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.
Transparency log