Skip to main content

irvalidate: Validate Iranian data. Zero dependencies.

English  ·  فارسی

CI PyPI Python versions Downloads Dependencies: 0 Typed License: MIT

Validate Iranian national IDs, bank cards, Sheba numbers, mobiles, landlines and postal codes,
in one tiny, typed, zero-dependency package.

اعتبارسنجی کد ملی، کارت، شبا، موبایل، تلفن ثابت و کد پستی؛ در یک پکیج کوچک، تایپ‌شده و بدون هیچ وابستگی


⚡ 30-second demo

from irvalidate import (
    validate_national_id, is_valid_sheba, card_bank, mobile_operator, landline_area,
)

validate_national_id("۰۰۱۳۵۴۲۴۱۹").value             # '0013542419'
validate_national_id("1234567890").code              # 'bad_checksum'
validate_national_id("1234567890").message_en        # 'national ID is invalid (checksum mismatch)'
is_valid_sheba("IR58 0540 1051 8002 1273 1130 07")  # True
card_bank("6037-9910-0000-0004").name_en             # 'Bank Melli Iran'
mobile_operator("+98 935 123 4567")                  # 'Irancell'
landline_area("021-22345678").province_en            # 'Tehran'

Persian digits, spaces, dashes and +98 prefixes just work. And when something is invalid, you get the reason, not just False.

$ pip install irvalidate

$ irvalidate card 6037991000000004
valid
bank: بانک ملی ایران (Bank Melli Iran)

$ irvalidate national-id 1234567890
invalid
reason: bad_checksum (national ID is invalid (checksum mismatch))

✨ Why irvalidate?

A copy-pasted snippet irvalidate
Persian / Arabic digits (۰-۹) usually ✗ ✅
Spaces, dashes, +98, 0098 in input do it yourself ✅
Real checksums (national ID, Luhn, mod-97) often just a regex ✅
Rejects 1111111111 and friends easy to forget ✅
Tells you why it failed (code + Persian/English message) ✗ ✅
Bank, mobile operator, province lookup ✗ ✅
Pydantic / FastAPI, Django, WTForms write it yourself ✅
Valid test-data generators ✗ ✅
Runtime dependencies n/a 0
Works offline (no network, no data leaves your machine) n/a ✅

🔍 What it validates

Data What is verified Extras
National ID (کد ملی) 10 digits, checksum, not all-same digits
Legal ID (شناسه ملی شرکت) 11 digits, checksum
Bank card (شماره کارت) 16 digits, Luhn issuing bank from 44 BIN prefixes
Sheba (شبا) IR + 24 digits, ISO 7064 mod-97 (IR is optional) bank from 36 bank codes
Mobile (موبایل) 09…, 9…, 98…, +98…, 0098… normalization, operator (MCI, Irancell, Rightel, Taliya, Shatel Mobile, Aptel)
Landline (تلفن ثابت) area code + 8-digit number province from all 31 area codes
Postal code (کد پستی) 10-digit format rules

📦 Install

pip install irvalidate

# optional framework integrations
pip install "irvalidate[pydantic]"   # Pydantic v2 / FastAPI
pip install "irvalidate[django]"
pip install "irvalidate[wtforms]"

Or straight from GitHub: pip install git+https://github.com/mehdirzfx/irvalidate.git

Requires Python 3.9+. The core has no dependencies.

🚀 Quick start

Every kind has an is_valid_* (returns bool) and a validate_* (returns a result with the reason).

from irvalidate import (
    is_valid_national_id, is_valid_legal_id, is_valid_card_number,
    is_valid_sheba, is_valid_mobile, is_valid_landline, is_valid_postal_code,
    normalize_mobile, normalize_landline,
)

is_valid_national_id("0013542419")           # True
is_valid_national_id("1234567890")           # False
is_valid_legal_id("14003778990")             # True
is_valid_card_number("6037-9910-0000-0004")  # True
is_valid_sheba("IR580540105180021273113007") # True
is_valid_mobile("+98 912 345 6789")          # True
is_valid_landline("021-22345678")            # True
is_valid_postal_code("1593764856")           # True

normalize_mobile("۰۹۱۲۳۴۵۶۷۸۹")               # '09123456789'
normalize_mobile("09123456789", international=True)  # '+989123456789'
normalize_landline("+982122345678")          # '02122345678'

🧭 Know why it failed

from irvalidate import validate_national_id, set_default_language

r = validate_national_id("1234567890")
bool(r)           # False
r.code            # 'bad_checksum'
r.message         # 'کد ملی نامعتبر است (رقم کنترلی اشتباه است)'
r.message_en      # 'national ID is invalid (checksum mismatch)'

ok = validate_national_id("۰۰۱۳۵۴۲۴۱۹")
ok.value          # '0013542419'
ok.raise_for_error()   # returns the normalized value, or raises ValidationError (a ValueError)

set_default_language("en")
r.message         # 'national ID is invalid (checksum mismatch)'

r.message follows the default language (Persian out of the box); message_fa and message_en are always available.

code Meaning
not_a_string the input isn't a str (e.g. an int, which would lose leading zeros)
empty nothing left after removing spaces and dashes
invalid_characters something other than digits
invalid_length wrong number of digits
all_same_digits e.g. 1111111111
bad_checksum checksum / Luhn / mod-97 mismatch
invalid_format wrong shape (mobile, landline, postal code)
unknown_area_code landline area code doesn't exist

🔌 Framework integrations

Pydantic / FastAPI

from fastapi import FastAPI
from pydantic import BaseModel
from irvalidate.integrations.pydantic import NationalId, MobileNumber

class User(BaseModel):
    national_id: NationalId   # "۰۰۱۳۵۴۲۴۱۹"       -> "0013542419"
    mobile: MobileNumber      # "+98 912 345 6789" -> "09123456789"

app = FastAPI()

@app.post("/users")
def create(user: User):
    return user

Invalid input returns a 422 whose message is the Persian (or English) reason. The values you receive are already normalized. Types: NationalId, LegalId, CardNumber, Sheba, MobileNumber, PostalCode, Landline.

Django

from django.db import models
from irvalidate.integrations.django import national_id_validator, mobile_validator

class Profile(models.Model):
    national_id = models.CharField(max_length=10, validators=[national_id_validator])
    mobile = models.CharField(max_length=14, validators=[mobile_validator])

The error code lands in ValidationError.code. Validators are plain module-level functions, so they serialize cleanly in migrations. Available: national_id_validator, legal_id_validator, card_number_validator, sheba_validator, mobile_validator, postal_code_validator, landline_validator.

WTForms

from wtforms import Form, StringField
from irvalidate.integrations.wtforms import NationalId, MobileNumber

class SignupForm(Form):
    national_id = StringField(validators=[NationalId()])
    mobile = StringField(validators=[MobileNumber(message="Invalid mobile number")])

🧪 Test-data generators

Need valid-looking fake data for tests and fixtures?

import random
from irvalidate import (
    generate_national_id, generate_legal_id, generate_card_number, generate_sheba,
    generate_mobile, generate_landline, generate_postal_code, is_valid_sheba,
)

generate_national_id()                 # e.g. 1043321810
generate_card_number(bank="Mellat")    # a valid card with a Mellat BIN
generate_sheba(bank="Parsian")         # a valid Sheba for Parsian
generate_mobile("Irancell")            # e.g. 09359141777
generate_landline("021")               # a Tehran landline

rng = random.Random(42)                # reproducible output
is_valid_sheba(generate_sheba(rng=rng))  # True

Generated values are valid in shape and checksum only, for tests and fake data. They may coincide with a real person's number, so never use them to impersonate anyone. The default random source is not cryptographically secure.

🎨 Format, mask & normalize

from irvalidate import (
    mask_card_number, format_card_number, format_sheba,
    format_mobile, format_landline, to_persian_digits,
)

mask_card_number("6037991000000004")        # '6037-99**-****-0004'
format_card_number("6037991000000004")      # '6037-9910-0000-0004'
format_sheba("IR580540105180021273113007")  # 'IR58 0540 1051 8002 1273 1130 07'
format_mobile("+989123456789")              # '0912 345 6789'
format_landline("02122345678")              # '021-22345678'
to_persian_digits("0912")                   # '۰۹۱۲'

💻 Command line

irvalidate national-id 0013542419
irvalidate sheba IR580540105180021273113007
irvalidate mobile +989351234567     # valid + operator: Irancell
irvalidate landline 02122345678     # valid + province: تهران (Tehran)
irvalidate card --generate          # print a valid test card number
# kinds: national-id, legal-id, card, sheba, mobile, landline, postal-code

Exit code is 0 for valid and 1 for invalid, so it works in shell scripts.

📚 API cheat sheet

Category Functions
Validate (bool) is_valid_national_id · is_valid_legal_id · is_valid_card_number · is_valid_sheba · is_valid_mobile · is_valid_landline · is_valid_postal_code
Validate (with reason) validate_national_id · validate_legal_id · validate_card_number · validate_sheba · validate_mobile · validate_landline · validate_postal_code → ValidationResult
Look up card_bank · sheba_bank · mobile_operator · landline_area
Normalize normalize_card_number · normalize_sheba · normalize_mobile · normalize_landline
Format / mask format_card_number · mask_card_number · format_sheba · format_mobile · format_landline
Digits to_english_digits · to_persian_digits
Generate generate_national_id · generate_legal_id · generate_card_number · generate_sheba · generate_mobile · generate_landline · generate_postal_code
Config / errors set_default_language · get_default_language · ValidationResult · ValidationError

Everything is fully type-annotated (py.typed).

⚠️ Good to know

  • Valid ≠ exists. Only structure and checksums are verified. The library can't tell whether a card or national ID belongs to a real person, and a postal code is checked for format only.
  • Pass strings, not numbers. A national ID like 0013542419 loses its leading zero as an int; non-string input is rejected with not_a_string.
  • Only the digits 0-9 and Persian/Arabic-Indic digits are accepted; other scripts' digits are rejected.
  • The lookup tables are best-effort. Bank BINs and Sheba codes, mobile operator prefixes and province area codes come from public information and may be incomplete or outdated (merged banks, new ranges). Corrections are very welcome.
  • Landlines: area code + 8-digit subscriber number. Old 7-digit numbers and service numbers (like 110) are not supported.

❓ FAQ

Does it call any server? No. Everything is a pure function: offline, nothing is sent anywhere.

Why does is_valid_national_id(13542419) return False? See "Pass strings, not numbers" above. The leading zeros are already gone by then.

Can I use it commercially? Yes, it's MIT-licensed.

How do I change the language of the messages? set_default_language("en") or "fa". Or read message_en / message_fa directly.

🤝 Contributing

Contributions are very welcome, especially data fixes (banks, BINs, operators, area codes) with a source link.

git clone https://github.com/mehdirzfx/irvalidate.git
cd irvalidate
pip install -e ".[dev]"
pytest

Bank and Sheba data live in src/irvalidate/banks.py, mobile operators in mobile.py and area codes in landline.py. See the changelog for what changed between versions.

If irvalidate saves you time, a ⭐ on GitHub helps other people find it.

📄 License

MIT. Free for personal and commercial use.

Metadata

Release files for irvalidate 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for irvalidate 0.2.1
File Size Uploaded Provenance Actions
irvalidate-0.2.1.tar.gz 31.2 kB With provenance Details for irvalidate-0.2.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for irvalidate 0.2.1
File Interpreter ABI Platform Actions
irvalidate-0.2.1-py3-none-any.whl Python 3 none any Details for irvalidate-0.2.1-py3-none-any.whl

Total release size: 59.4 kB

Release files / irvalidate-0.2.1.tar.gz

Download URL irvalidate-0.2.1.tar.gz
Size 31.2 kB
Tags Source
SHA-256 checksum
How to use checksums
38205047e75bde6cdc5a2826520207df9216d17825d152437cccaa119afee099
BLAKE2b-256 checksum
How to use checksums
d4e10c830660400474f1d96c73e58d810f889b4117986cf9a0020c9fc62ffccb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 31, 2026.

Transparency log

Release files / irvalidate-0.2.1-py3-none-any.whl

Download URL irvalidate-0.2.1-py3-none-any.whl
Size 28.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6c21cee11a409b014ce1a3d914796cda7130b0153302cbcd4a5f4c307e92551d
BLAKE2b-256 checksum
How to use checksums
cc5472c2fa3c3ecccfde1c75256de750165d7e194adaf560ebec496450234ef1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 31, 2026.

Transparency log

Security for irvalidate 0.2.1

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for irvalidate-0.2.1.tar.gz

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 Oct 31, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page