Guide · Python · SDK · REST API

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.

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:

PositionsLengthDescription
1–22Year of birth (last two digits)
3–42Month of birth (century-encoded — see table below)
5–62Day of birth
7–93Serial number (ordinal within the same birth date)
101Gender digit (odd = male, even = female)
111Check 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:

CenturyYear rangeMonth offsetMonth digits range
19th1800–1899+8081–92
20th1900–1999+001–12
21st2000–2099+2021–32
22nd2100–2199+4041–52
23rd2200–2299+6061–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.

ℹ️Gender is determined by the 10th digit (position index 9). An odd digit means male, an even digit means female. In 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).

  1. Multiply each of the first 10 digits by its weight: [1, 3, 7, 9, 1, 3, 7, 9, 1, 3]
  2. Sum all products
  3. Calculate check = (10 − (sum mod 10)) mod 10
  4. 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:

PositionDigitWeightProduct
1414
24312
3070
45945
5111
64312
7070
8199
9313
105315

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).

⚠️A PESEL that passes the checksum is not necessarily real. The checksum only catches accidental transcription errors — it does not confirm that the number was ever actually issued. Only an authoritative registry can do that.

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"],
    )
The SDK handles authentication, retries, and response parsing for you. For production applications, prefer the SDK over raw HTTP calls.

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 }
FieldTypeDescription
validbooleanWhether the PESEL passed format, date, and checksum validation
birthDatestringISO 8601 date extracted from the PESEL (e.g. "1990-09-05"). Only present when valid: true
genderstring"male" or "female", derived from the 10th digit. Only present when valid: true
isOver15booleanWhether the person is 15 or older based on the decoded birth date
isOver18booleanWhether the person is 18 or older — useful for legal age checks
isOver21booleanWhether the person is 21 or older — used in some regulatory contexts
ℹ️The age threshold fields (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

Do not store PESEL as an integer — leading zeros will be lost
Do not skip calendar date validation — a valid checksum can encode Feb 30
Do not ignore the century offset — month digits above 12 are valid for post-1999 births
Validate checksum, date, and century encoding together — the API handles all three
Use isOver18 / isOver21 for age gating — no manual date math needed
Extract gender and birth date from the response — no PESEL parsing in your code

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.