Parse AAMVA PDF417 barcode data from US and Canadian driver's licenses and ID cards (Python port of aamva-parser).
Project description
py-aamva-parser
Python library to parse AAMVA PDF417 barcode payloads from US and Canadian driver's licenses and ID cards.
This project is a Python port of aamva-parser by joptimus, originally implemented in TypeScript for Node.js. Parsing logic, version-specific field maps, and helpers are intended to track that upstream project.
The port was produced with AI assistance and reviewed by a human before publication.
Supports AAMVA barcode versions 01–12 (CDS 2000–2025). Includes helpers for age checks, full name formatting, and CDL detection (v12).
Requirements
- Python 3.10+
Installation
The PyPI / pip name is aamva-parser (hyphen). The import name is aamva_parser (underscore): import aamva_parser or from aamva_parser import parse.
From PyPI:
pip install aamva-parser
From a git checkout:
pip install .
pip install -e ".[dev]" # editable, with dev dependencies
With Poetry (2.0+ recommended; the package is PEP 621 metadata with setuptools as the build backend):
From PyPI:
poetry add aamva-parser
From a local path or Git URL:
poetry add git+https://github.com/btmash/py-aamva-parser.git
The importable package name is aamva_parser (underscore).
Usage
Parse a barcode
from aamva_parser import parse, get_version, is_expired, get_age, is_under_21, get_full_name
barcode_data = """
@
ANSI 636026080102DL00410288ZA03290015DLDAQD12345678
DCSPUBLIC
DDEN
DACJOHN
DDFN
DADQUINCY
DDGN
DCAD
DCBNONE
DCDNONE
DBD08242015
DBB01311970
DBA01312035
DBC1
DAU069 in
DAYGRN
DAG789 E OAK ST
DAIANYTOWN
DAJCA
DAK902230000
DCF83D9BN217QO983B1
DCGUSA
DAW180
DAZBRO
DCK12345678900000000000
DDB02142014
DDK1
ZAZAAN
ZAB
ZAC
"""
lic = parse(barcode_data)
print(lic.first_name) # "JOHN"
print(lic.last_name) # "PUBLIC"
print(lic.date_of_birth) # datetime.datetime(1970, 1, 31, 0, 0)
print(lic.expiration_date) # datetime.datetime(2035, 1, 31, 0, 0)
print(lic.expired) # False (snapshot from parsed expiry vs "now")
age = get_age(barcode_data)
under_21 = is_under_21(barcode_data)
name = get_full_name(barcode_data) # "JOHN QUINCY PUBLIC"
expired = is_expired(barcode_data)
version = get_version(barcode_data) # "08"
Jurisdiction, CDL, and “acceptable” checks
from aamva_parser import get_state, is_cdl, is_acceptable, is_under_18
state = get_state(barcode_data) # e.g. "CA", or None
cdl = is_cdl(barcode_data) # True when v12 CDL indicator is set
ok = is_acceptable(barcode_data) # issued, not expired, required fields present
minor = is_under_18(barcode_data) # False if 18+ or no DOB
Instance methods on License
After lic = parse(...), you can call methods on the same object instead of re-parsing:
lic = parse(barcode_data)
if lic.is_expired():
...
if lic.has_been_issued() and lic.is_acceptable():
...
Types, annotations, and enums
parse() returns a License dataclass. ParsedLicense is a typing.TypeAlias to License for annotations; at runtime isinstance(lic, License) and isinstance(lic, ParsedLicense) are equivalent.
from aamva_parser import License, ParsedLicense, Gender, EyeColor, parse
def summarize(card: ParsedLicense) -> str:
return f"{card.first_name} {card.last_name} ({card.state})"
lic: License = parse(barcode_data)
if lic.gender == Gender.MALE:
print("Male")
if lic.eye_color == EyeColor.GREEN:
print("Green eyes")
Package version
import aamva_parser
print(aamva_parser.__version__)
LicenseParser class
For multiple operations on the same raw string:
from aamva_parser import LicenseParser
parser = LicenseParser(barcode_data)
version = parser.parse_version()
lic = parser.parse()
Each module-level helper (get_age, is_expired, …) parses the string again. Prefer LicenseParser when you need the version, a License, and several helpers, so you pay for one parse.
API
The package exports License (concrete result type), ParsedLicense (type alias to License for annotations), enums, and LicenseParser. Module-level functions:
Each helper such as get_full_name(barcode) or is_acceptable(barcode) runs parse(barcode) internally. For hot paths, build one LicenseParser and call parse() once, then read fields or call License methods.
Deprecated Parse / GetVersion / IsExpired (PascalCase) are re-exported from the package root; prefer snake_case.
| Function | Returns | Description |
|---|---|---|
parse(barcode_data) |
License |
Parse PDF417 payload into a License. |
get_version(barcode_data) |
str | None |
AAMVA version (e.g. "08"). |
is_expired(barcode_data) |
bool |
Whether the expiration date is in the past. |
get_age(barcode_data) |
int | None |
Age in years, or None without DOB. |
is_under_21(barcode_data) |
bool |
Under 21, or False if 21+ or no DOB. |
is_under_18(barcode_data) |
bool |
Under 18, or False if 18+ or no DOB. |
is_acceptable(barcode_data) |
bool |
Not expired, issued, and required fields set. |
get_full_name(barcode_data) |
str | None |
"FIRST MIDDLE LAST"; None if no names. |
get_state(barcode_data) |
str | None |
Jurisdiction (e.g. "CA"). |
is_cdl(barcode_data) |
bool |
CDL indicator set (v12 / CDS 2025). |
Deprecated aliases
| Deprecated | Use instead |
|---|---|
Parse() |
parse() |
GetVersion() |
get_version() |
IsExpired() |
is_expired() |
License methods
is_expired()— compareexpiration_dateto the current time.has_been_issued()—Trueifissue_dateis set and the current time is after it.is_acceptable()— stricter checklist (name, address, dates, document ID, etc.).
Supported fields (License)
| Field | Type | Attribute |
|---|---|---|
| First name | str | None |
first_name |
| Last name | str | None |
last_name |
| Middle name | str | None |
middle_name |
| Expiration date | datetime | None |
expiration_date |
| Issue date | datetime | None |
issue_date |
| Date of birth | datetime | None |
date_of_birth |
| Gender | Gender |
gender |
| Eye color | EyeColor |
eye_color |
| Hair color | HairColor |
hair_color |
| Height (inches) | float | None |
height |
| Weight | str | None |
weight |
| Street address | str | None |
street_address |
| Street address line 2 | str | None |
street_address_supplement |
| City | str | None |
city |
| State | str | None |
state |
| Postal code | str | None |
postal_code |
| Driver's license ID | str | None |
drivers_license_id |
| Document ID | str | None |
document_id |
| Issuing country | IssuingCountry |
country |
| Name suffix | NameSuffix |
suffix |
| First name truncation | Truncation |
first_name_truncation |
| Middle name truncation | Truncation |
middle_name_truncation |
| Last name truncation | Truncation |
last_name_truncation |
| Place of birth | str | None |
place_of_birth |
| Audit information | str | None |
audit_information |
| Inventory control number | str | None |
inventory_control_number |
| First name alias | str | None |
first_name_alias |
| Last name alias | str | None |
last_name_alias |
| Suffix alias | str | None |
suffix_alias |
| CDL indicator | str | None |
cdl_indicator |
| Non-domiciled indicator | str | None |
non_domiciled_indicator |
| Enhanced credential indicator | str | None |
enhanced_credential_indicator |
| Permit indicator | str | None |
permit_indicator |
| Expired (parsed snapshot) | bool |
expired |
| AAMVA version | str | None |
version |
| Raw barcode | str | None |
pdf417 |
AAMVA version support
| CDS | Year | Barcode | Supported |
|---|---|---|---|
| 2000 | 2000 | 01 | Yes |
| 2003 | 2003 | 02 | Yes |
| 2005 | 2005 | 03 | Yes |
| 2009 | 2009 | 04–05 | Yes |
| 2010 | 2010 | 06 | Yes |
| 2011 | 2011 | 07 | Yes |
| 2012 | 2012 | 08 | Yes |
| 2013 | 2013 | 09 | Yes |
| 2016 | 2016 | 10 | Yes |
| 2020 | 2020 | 11 | Yes |
| 2025 | 2025 | 12 | Yes |
Example
Raw PDF417 payload (version 08)
@
ANSI 636026080102DL00410288ZA03290015DLDAQD12345678
DCSPUBLIC
DDEN
DACJOHN
DDFN
DADQUINCY
DDGN
DCAD
DCBNONE
DCDNONE
DBD08242015
DBB01311970
DBA01312035
DBC1
DAU069 in
DAYGRN
DAG789 E OAK ST
DAIANYTOWN
DAJCA
DAK902230000
DCF83D9BN217QO983B1
DCGUSA
DAW180
DAZBRO
DCK12345678900000000000
DDB02142014
DDK1
ZAZAAN
ZAB
ZAC
Parsed values (illustrative)
# After lic = parse(barcode_data)
{
"first_name": "JOHN",
"last_name": "PUBLIC",
"middle_name": "QUINCY",
"date_of_birth": datetime(1970, 1, 31),
"expiration_date": datetime(2035, 1, 31),
"issue_date": datetime(2015, 8, 24),
"gender": Gender.MALE,
"eye_color": EyeColor.GREEN,
"hair_color": HairColor.BROWN,
"height": 69,
"weight": "180",
"street_address": "789 E OAK ST",
"city": "ANYTOWN",
"state": "CA",
"postal_code": "902230000",
"drivers_license_id": "D12345678",
"document_id": "83D9BN217QO983B1",
"country": IssuingCountry.UNITED_STATES,
"inventory_control_number": "12345678900000000000",
"expired": False,
"version": "08",
}
AAMVA element IDs by version
Bold = mandatory in upstream docs. -- = not used in that barcode version.
Tables are split so they fit typical viewports; the two halves are the same data as one wide 01–12 grid.
Versions 01–06
| Field | 01 | 02 | 03 | 04 | 05 | 06 |
|---|---|---|---|---|---|---|
| First Name | DAC | DCT | DCT | DAC | DAC | DAC |
| Last Name | DAB | DCS | DCS | DCS | DCS | DCS |
| Middle Name | DAD | DAD | DAD | DAD | DAD | DAD |
| Expiration Date | DBA | DBA | DBA | DBA | DBA | DBA |
| Issue Date | DBD | DBD | DBD | DBD | DBD | DBD |
| Date of Birth | DBB | DBB | DBB | DBB | DBB | DBB |
| Gender | DBC | DBC | DBC | DBC | DBC | DBC |
| Eye Color | DAY | DAY | DAY | DAY | DAY | DAY |
| Height | DAU | DAU | DAU | DAU | DAU | DAU |
| Street Address | DAG | DAG | DAG | DAG | DAG | DAG |
| City | DAI | DAI | DAI | DAI | DAI | DAI |
| State | DAJ | DAJ | DAJ | DAJ | DAJ | DAJ |
| Postal Code | DAK | DAK | DAK | DAK | DAK | DAK |
| License ID | DBJ | DAQ | DAQ | DAQ | DAQ | DAQ |
| Document ID | -- |
DCF | DCF | DCF | DCF | DCF |
| Country | -- |
DCG | DCG | DCG | DCG | DCG |
| Weight | -- |
DAW | DAW | DAW | DAW | DAW |
| CDL Indicator | -- |
-- |
-- |
-- |
-- |
-- |
| Non-Domiciled Indicator | -- |
-- |
-- |
-- |
-- |
-- |
| Enhanced Credential | -- |
-- |
-- |
-- |
-- |
-- |
| Permit Indicator | -- |
-- |
-- |
-- |
-- |
-- |
Versions 07–12
| Field | 07 | 08 | 09 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|
| First Name | DAC | DAC | DAC | DAC | DAC | DAC |
| Last Name | DCS | DCS | DCS | DCS | DCS | DCS |
| Middle Name | DAD | DAD | DAD | DAD | DAD | DAD |
| Expiration Date | DBA | DBA | DBA | DBA | DBA | DBA |
| Issue Date | DBD | DBD | DBD | DBD | DBD | DBD |
| Date of Birth | DBB | DBB | DBB | DBB | DBB | DBB |
| Gender | DBC | DBC | DBC | DBC | DBC | DBC |
| Eye Color | DAY | DAY | DAY | DAY | DAY | DAY |
| Height | DAU | DAU | DAU | DAU | DAU | DAU |
| Street Address | DAG | DAG | DAG | DAG | DAG | DAG |
| City | DAI | DAI | DAI | DAI | DAI | DAI |
| State | DAJ | DAJ | DAJ | DAJ | DAJ | DAJ |
| Postal Code | DAK | DAK | DAK | DAK | DAK | DAK |
| License ID | DAQ | DAQ | DAQ | DAQ | DAQ | DAQ |
| Document ID | DCF | DCF | DCF | DCF | DCF | DCF |
| Country | DCG | DCG | DCG | DCG | DCG | DCG |
| Weight | DAW | DAW | DAW | DAW | DAW | DAW |
| CDL Indicator | -- |
-- |
-- |
-- |
-- |
DDM |
| Non-Domiciled Indicator | -- |
-- |
-- |
-- |
-- |
DDN |
| Enhanced Credential | -- |
-- |
-- |
-- |
-- |
DDO |
| Permit Indicator | -- |
-- |
-- |
-- |
-- |
DDP |
Development
Automated tests mirror the upstream Jest layout under js-aamva-parser/tests/ (for example regex.test.ts → tests/test_regex.py, indexApi.test.ts → tests/test_index_api.py). Deprecated PascalCase helpers are implemented in src/aamva_parser/compat.py.
pip and venv
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
ruff check src tests
ruff format --check src tests
mypy
pytest
Poetry
From the repository root (Poetry 2.0+):
poetry install -E dev
poetry run ruff check src tests
poetry run ruff format --check src tests
poetry run mypy
poetry run pytest
Optional: poetry shell, then run the same commands without the poetry run prefix.
License
ISC
Credits
- Python port: maintained by btmash (py-aamva-parser).
- Upstream: aamva-parser by joptimus (TypeScript / Node.js).
- The original JavaScript README credits inspiration from the Swift project ksoftllc/license-parser.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aamva_parser-0.1.2.tar.gz.
File metadata
- Download URL: aamva_parser-0.1.2.tar.gz
- Upload date:
- Size: 23.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2798dd72b35c1a062c60672df8f95b5968913bb5968c43f67f3173dc26e5814c
|
|
| MD5 |
48f39108a491084088561f83f166ef0b
|
|
| BLAKE2b-256 |
e1e863015cbaf885d75e29dd47bf0666848c0f3e65ffefd58dca2dcd940c1691
|
Provenance
The following attestation bundles were made for aamva_parser-0.1.2.tar.gz:
Publisher:
publish.yml on btmash/py-aamva-parser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aamva_parser-0.1.2.tar.gz -
Subject digest:
2798dd72b35c1a062c60672df8f95b5968913bb5968c43f67f3173dc26e5814c - Sigstore transparency entry: 1365325476
- Sigstore integration time:
-
Permalink:
btmash/py-aamva-parser@2ce2e54e1fab036a32045ead8430cf1c2d1f1ecd -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/btmash
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2ce2e54e1fab036a32045ead8430cf1c2d1f1ecd -
Trigger Event:
release
-
Statement type:
File details
Details for the file aamva_parser-0.1.2-py3-none-any.whl.
File metadata
- Download URL: aamva_parser-0.1.2-py3-none-any.whl
- Upload date:
- Size: 16.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
864c387466b9a318cf79ea59357b8089f030b7bede27d3ace7065a10f98bc3da
|
|
| MD5 |
8d6329bfc3ce1816feae40e6db88493c
|
|
| BLAKE2b-256 |
8466697467c01f36eb1f6ac1b8f491e207cf0bce061cb62cbb55e474c550ecf6
|
Provenance
The following attestation bundles were made for aamva_parser-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on btmash/py-aamva-parser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aamva_parser-0.1.2-py3-none-any.whl -
Subject digest:
864c387466b9a318cf79ea59357b8089f030b7bede27d3ace7065a10f98bc3da - Sigstore transparency entry: 1365325523
- Sigstore integration time:
-
Permalink:
btmash/py-aamva-parser@2ce2e54e1fab036a32045ead8430cf1c2d1f1ecd -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/btmash
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2ce2e54e1fab036a32045ead8430cf1c2d1f1ecd -
Trigger Event:
release
-
Statement type: