English · فارسی
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
0013542419loses its leading zero as anint; non-string input is rejected withnot_a_string. - Only the digits
0-9and 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.