PESEL Validation in Python
The Polish national identification number — 11 digits encoding birth date, gender, and a checksum. Here's how to validate it correctly in Python and extract the demographic data it carries.
In this guide
1. What is a PESEL number?
PESEL (Powszechny Elektroniczny System Ewidencji Ludnosci) is the 11-digit national identification number assigned to every person registered in Poland — both Polish citizens and foreign residents. It is issued at birth (or upon registration for foreigners) and remains unchanged for life. PESEL appears on:
- National ID cards (dowod osobisty)
- Tax declarations and invoices
- Social security (ZUS) records
- Medical prescriptions and health insurance
- Bank account applications and KYC forms
Unlike many national IDs that are opaque serial numbers, PESEL encodes real demographic data: date of birth, gender, and a serial number — all verifiable through its built-in checksum.
2. PESEL anatomy
A PESEL number is exactly 11 digits long. Each group of digits carries specific meaning:
| Positions | Length | Description |
|---|---|---|
| 1–2 | 2 | Year of birth (last two digits) |
| 3–4 | 2 | Month of birth (century-encoded — see table below) |
| 5–6 | 2 | Day of birth |
| 7–9 | 3 | Serial number (ordinal within the same birth date) |
| 10 | 1 | Gender digit (odd = male, even = female) |
| 11 | 1 | Check digit (computed over digits 1–10) |
Century encoding
Because two digits are not enough to represent the full year, PESEL encodes the century by adding an offset to the month value. This allows the system to handle dates from 1800 all the way through 2299:
| Century | Year range | Month offset | Month digits range |
|---|---|---|---|
| 19th | 1800–1899 | +80 | 81–92 |
| 20th | 1900–1999 | +0 | 01–12 |
| 21st | 2000–2099 | +20 | 21–32 |
| 22nd | 2100–2199 | +40 | 41–52 |
| 23rd | 2200–2299 | +60 | 61–72 |
For example, the PESEL 44051401358 has month digits 05, which falls in the 01–12 range — meaning this person was born in May of a year in the 1900s. Combined with the year digits 44, the full birth date is 14 May 1944.
44051401358, the 10th digit is 5 (odd), so the person is male.3. The checksum algorithm
PESEL uses a weighted MOD-10 checksum. Each of the first 10 digits is multiplied by a fixed weight, and the sum of products modulo 10 produces a value that, combined with the check digit, must equal 0 (mod 10).
- Multiply each of the first 10 digits by its weight:
[1, 3, 7, 9, 1, 3, 7, 9, 1, 3] - Sum all products
- Calculate
check = (10 − (sum mod 10)) mod 10 - Compare the result with the 11th digit
Here is a straightforward Python implementation:
def validate_pesel(pesel: str) -> bool: """Validate a Polish PESEL number using the MOD-10 checksum. Args: pesel: An 11-digit PESEL string. Returns: True if the checksum is valid, False otherwise. """ if len(pesel) != 11 or not pesel.isdigit(): return False digits = [int(c) for c in pesel] weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3] weighted_sum = sum(d * w for d, w in zip(digits, weights)) check = (10 - (weighted_sum % 10)) % 10 return check == digits[10]
Let's walk through the PESEL 44051401358 step by step:
| Position | Digit | Weight | Product |
|---|---|---|---|
| 1 | 4 | 1 | 4 |
| 2 | 4 | 3 | 12 |
| 3 | 0 | 7 | 0 |
| 4 | 5 | 9 | 45 |
| 5 | 1 | 1 | 1 |
| 6 | 4 | 3 | 12 |
| 7 | 0 | 7 | 0 |
| 8 | 1 | 9 | 9 |
| 9 | 3 | 1 | 3 |
| 10 | 5 | 3 | 15 |
Sum = 4 + 12 + 0 + 45 + 1 + 12 + 0 + 9 + 3 + 15 = 101
(10 − (101 mod 10)) mod 10 = (10 − 1) mod 10 = 9 mod 10 = 9
But the 11th digit of 44051401358 is 8.
9 ≠ 8 — so this particular PESEL would fail the checksum (it is a fictitious example for illustration).
4. Why manual validation isn't enough
Checksum validation only catches typos
The MOD-10 checksum detects single-digit errors and most transposition errors. It cannot tell you whether the PESEL was actually assigned to a real person. Anyone can generate an 11-digit string that passes the checksum formula — that does not make it a valid national ID.
Date extraction requires century decoding
Extracting the birth date from a PESEL requires handling the century offset table correctly. A naive implementation that ignores the month offset will produce wrong dates for anyone born after 1999 (month digits 21–32) or before 1900 (month digits 81–92). Getting this wrong in a KYC flow means rejecting valid customers.
Age verification needs a reliable birth date
Many business workflows need to verify whether the person is over 15, 18, or 21 years old. Doing this manually means you need to decode the PESEL, compute the birth date, then compare it against today's date accounting for leap years and edge cases. One API call handles all of this.
You still need to validate the date itself
A PESEL can pass the checksum but encode an impossible date — for example, February 30 or month 13. A robust validator must check that the decoded date is a real calendar date, including leap year rules.
5. Python code example
Using the isvalid-sdk Python SDK or the requests library. Install with pip install isvalid-sdk or pip install requests.
# pesel_validator.py import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) # ── Validate a PESEL number ─────────────────────────────────────────────── result = iv.pl.pesel("44051401358") if not result["valid"]: print("Invalid PESEL") else: print(f"Birth date : {result['birthDate']}") print(f"Gender : {result['gender']}") print(f"Over 18 : {result['isOver18']}") print(f"Over 21 : {result['isOver21']}")
In a web application, you might expose a PESEL validation endpoint with Flask:
# app.py (Flask) import os from flask import Flask, request, jsonify from isvalid_sdk import IsValidConfig, create_client app = Flask(__name__) iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) @app.post("/verify-identity") def verify_identity(): data = request.get_json() pesel = data.get("pesel") if not pesel: return jsonify(error="PESEL is required"), 400 try: result = iv.pl.pesel(pesel) except Exception: return jsonify(error="PESEL validation service unavailable"), 502 if not result["valid"]: return jsonify(error="Invalid PESEL number"), 400 if not result.get("isOver18"): return jsonify(error="Person must be at least 18 years old"), 403 return jsonify( valid=True, birthDate=result["birthDate"], gender=result["gender"], isOver18=result["isOver18"], isOver21=result["isOver21"], )
6. cURL example
Validate a PESEL number from the command line:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/pesel?value=44051401358"
If the PESEL is valid, the API returns the decoded birth date, gender, and age thresholds:
{ "valid": true, "birthDate": "1990-09-05", "gender": "male", "isOver15": true, "isOver18": true, "isOver21": true }
7. Understanding the response
Valid PESEL
{ "valid": true, "birthDate": "1990-09-05", "gender": "male", "isOver15": true, "isOver18": true, "isOver21": true }
Invalid PESEL
{ "valid": false }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the PESEL passed format, date, and checksum validation |
| birthDate | string | ISO 8601 date extracted from the PESEL (e.g. "1990-09-05"). Only present when valid: true |
| gender | string | "male" or "female", derived from the 10th digit. Only present when valid: true |
| isOver15 | boolean | Whether the person is 15 or older based on the decoded birth date |
| isOver18 | boolean | Whether the person is 18 or older — useful for legal age checks |
| isOver21 | boolean | Whether the person is 21 or older — used in some regulatory contexts |
isOver15, isOver18, isOver21) are computed server-side at the time of the API call. You do not need to compare dates yourself.8. Edge cases
Persons born after 1999
People born in the 2000s have month digits in the 21–32 range. A PESEL like 02271409862 encodes month 27, which means July 2002 (27 − 20 = 7). If your manual validator does not handle the century offset, it will reject every PESEL issued after 1999.
# Born in 2002 — month digits are 27 (July, 21st century offset) result = iv.pl.pesel("02271409862") print(result["birthDate"]) # "2002-07-14" print(result["gender"]) # "female" (digit 10 is 6, even)
Invalid calendar dates
A PESEL can pass the MOD-10 checksum but encode February 30, April 31, or other impossible dates. The API validates the decoded date against the real calendar and returns { "valid": false } for impossible dates, even when the checksum is correct.
# Checksum might pass, but Feb 30 does not exist result = iv.pl.pesel("90023012345") print(result["valid"]) # False — invalid date
Leading zeros and string handling
PESEL numbers can start with 0 (anyone born in the 2000s has year digits starting with 0). Never store or transmit PESEL as an integer — always use strings. Casting to int strips the leading zero and changes the number of digits.
# WRONG — loses the leading zero pesel = int("02271409862") # 2271409862 — 10 digits, invalid # CORRECT — preserve as string pesel = "02271409862" # 11 digits, ready for validation
Age threshold on the exact birthday
The isOver18 field becomes true on the person's 18th birthday, not the day after. The API uses server-side date comparison so you do not need to worry about time zones or off-by-one errors in your own code.
# Person turning 18 today result = iv.pl.pesel("08031112345") if result["valid"]: if result["isOver18"]: print("Person is 18 or older — proceed") else: print("Person is under 18 — access denied")
Batch validation
When validating multiple PESELs (e.g. importing a CSV), use concurrent.futures to parallelize API calls and avoid sequential bottlenecks:
import os from concurrent.futures import ThreadPoolExecutor, as_completed from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) pesel_numbers = ["44051401358", "02271409862", "90090515836"] with ThreadPoolExecutor(max_workers=5) as pool: futures = {pool.submit(iv.pl.pesel, p): p for p in pesel_numbers} for future in as_completed(futures): pesel = futures[future] result = future.result() status = "valid" if result["valid"] else "invalid" print(f"{pesel}: {status}")
Summary
See also
Validate PESEL numbers instantly
Free tier includes 100 API calls per day. No credit card required. Checksum validation, birth date extraction, and age verification included.