pydantic-extra-types
pydantic-extra-types is the official overflow bin for pydantic field types that the maintainers did not want inside pydantic itself. Its own README describes it as a place for types that probably should not exist in the main library. You get around thirty ready-made annotations you would otherwise hand-roll: PhoneNumber, CountryAlpha2 and friends, PaymentCardNumber with Luhn checking and brand detection, Color, Latitude and Longitude, MacAddress, ISBN, ISIN, IBAN, ULID, UUID6 through UUID8, TimeZoneName, ISO 4217 currency codes, ISO 639 language codes, S3 paths, cron strings, MongoDB ObjectIds, and epoch timestamps. Each type validates on input, serializes to something sensible, and contributes a JSON Schema entry, so FastAPI docs and generated clients pick it up automatically. Almost all of it depends on pydantic v2 only, but the interesting types need an optional third-party package installed alongside.
Useful, well-scoped glue that saves you from writing check-digit and ISO-registry validators, and the pydantic org name means it keeps up with pydantic core changes. Install it with the specific extras you need rather than [all], and treat it as a convenience layer over phonenumbers and pycountry rather than as a validation authority.
Use it if
- You are writing the same regex or check-digit validator for the fourth time: this ships tested implementations of Luhn, ISBN-10 and ISBN-13, ISIN, IBAN, and the ISO country, currency, and language registries
- You use FastAPI and want the OpenAPI schema and the generated clients to describe a field as a phone number or a country code, not as a bare string
- You need phone parsing that follows Google libphonenumber rules (region defaults, national vs E164 formatting, region allowlists) but you want it as a pydantic annotation rather than glue code in a validator
- You are upgrading from pydantic v1 and your models used Color or PaymentCardNumber, which were removed from pydantic v2 core and moved here
- You need one type. Installing a grab-bag of thirty to get MacAddress adds a dependency your security team has to track for a class that is about forty lines of regex; copy the pattern into your codebase instead
- You cannot accept the optional dependencies' weight. phonenumbers carries the whole libphonenumber metadata set, pycountry bundles the ISO databases, pendulum and pymongo are full libraries; the extras are not free even though the base package is
- You expect a normal ImportError when an extra is missing. Importing pydantic_extra_types.phone_numbers without phonenumbers raises a RuntimeError at import time, which crashes module loading rather than degrading, so an incomplete requirements file breaks the app at boot rather than at first use
- You want stable release hygiene here. Version 2.11.2 was yanked in April 2026 with the reason 'multiple feature mistakenly merged before release', so the newest published version is not always the one to pin
- You believe format validation equals business validation. PaymentCardNumber checks digits, length, Luhn, and brand prefix; it says nothing about whether the card exists or has funds. Same for IBAN and ISIN: structurally valid is not the same as real
- You are still on pydantic v1. This package requires pydantic 2.5.2 or newer and uses the v2 core schema protocol throughout, with no v1 compatibility path
Setup reality
pip install pydantic-extra-types gets you pydantic and typing-extensions and only the types with no third-party dependency: Color, Coordinate, MacAddress, ISBN, ISIN, PaymentCardNumber, DomainStr, S3Path, epoch, MongoObjectId, the resolved Path types, the DSN aliases, and UUID6 through UUID8. Everything else needs an extra, and the extra name is not always the type name: phone_numbers wants pydantic-extra-types[phonenumbers], country, currency_code, and language_code want [pycountry], SemanticVersion wants [semver], ULID wants [python-ulid], CronStr wants [cron], and the pendulum date types want [pendulum]. There is a catch-all pip install 'pydantic-extra-types[all]' if you would rather not think about it, at the cost of pulling in pycountry, phonenumbers, pendulum, pymongo, semver, pytz, tzdata, and uuid-utils. Missing extras raise a RuntimeError during import of the submodule, not a clean fallback, so add the extra to your lockfile the moment you add the import. On slim container images TimeZoneName also needs the tzdata package, because the base image often has no system zoneinfo database.
Patterns
Install only the extras the types you import needinstall-the-right-extra
pip install pydantic-extra-types # dependency-free types only
pip install "pydantic-extra-types[phonenumbers]" # PhoneNumber
pip install "pydantic-extra-types[pycountry]" # Country, Currency, Language
pip install "pydantic-extra-types[python-ulid,semver]"
pip install "pydantic-extra-types[all]" # everything, including pymongoThe extras are listed only in pyproject.toml, and the names do not always match the module (phone_numbers needs the phonenumbers extra, cron needs cron). Importing a submodule whose extra is missing raises RuntimeError at import time, so a wrong lockfile fails at process start.
Validate and normalise a phone numberphone-number-field
from pydantic import BaseModel
from pydantic_extra_types.phone_numbers import PhoneNumber
class Contact(BaseModel):
name: str
phone: PhoneNumber
c = Contact(name='Alice', phone='+1 650-253-0000')
print(c.phone) # tel:+1-650-253-0000The default output format is RFC3966, which means a tel: prefix that surprises people expecting E164. The number must be parseable and valid, not merely digit-shaped, because phonenumbers checks it against the real numbering plan for the region.
Change phone formatting and restrict regionsphone-number-e164
from typing import Annotated, Union
import phonenumbers
from pydantic import BaseModel
from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator
E164 = Annotated[Union[str, phonenumbers.PhoneNumber],
PhoneNumberValidator(number_format='E164', default_region='US')]
class USPhone(PhoneNumber):
default_region_code = 'US'
supported_regions = ['US']
phone_format = 'NATIONAL'
class Model(BaseModel):
phone: E164
print(Model(phone='650-253-0000').phone) # +16502530000Two ways to configure the same thing: subclass PhoneNumber for class-level defaults, or use the PhoneNumberValidator annotation for per-field settings. Setting default_region is what lets a national number without a country prefix parse at all.
Accept and convert ISO country codescountry-codes
from pydantic import BaseModel
from pydantic_extra_types.country import (
CountryAlpha2, CountryAlpha3, CountryNumericCode, CountryShortName,
)
class Product(BaseModel):
made_in: CountryAlpha2
p = Product(made_in='ES')
print(p.made_in.alpha3, p.made_in.short_name)Each variant validates one representation and exposes the others as properties, so CountryAlpha2 can hand you the alpha-3 code and the English name. Values come from pycountry, meaning the accepted list moves when pycountry updates its ISO data snapshot.
Check a card number and read its BIN and brandpayment-card-number
from pydantic import BaseModel
from pydantic_extra_types.payment import PaymentCardNumber, PaymentCardBrand
class Payment(BaseModel):
card: PaymentCardNumber
p = Payment(card='4000000000000002')
print(p.card.bin, p.card.last4, p.card.brand)
print(p.card.brand is PaymentCardBrand.visa)This ran Luhn, a 12 to 19 digit length check, and prefix-based brand detection. That is all. Never log or store the full value you just validated, and do not treat a pass as any signal that the card is chargeable.
Parse colours in any common notationcolor-values
from pydantic import BaseModel
from pydantic_extra_types.color import Color
class Theme(BaseModel):
accent: Color
t = Theme(accent='rgb(38, 200, 122)')
print(t.accent.as_hex()) # '#26c87a'
print(t.accent.as_rgb_tuple()) # (38, 200, 122)
print(t.accent.original()) # 'rgb(38, 200, 122)'This is the pydantic v1 Color class, moved here when v2 dropped it. It accepts named CSS colours, hex in short and long form, rgb, rgba, hsl, and hsla, and original() keeps the string the user actually typed. Model dumps serialize it back to a string, not to a tuple.
Bound geographic coordinates at the type levellatitude-longitude
from pydantic import BaseModel
from pydantic_extra_types.coordinate import Coordinate, Latitude, Longitude
class Place(BaseModel):
lat: Latitude
lon: Longitude
where: Coordinate
p = Place(lat=22.3, lon=70.8, where=(22.3, 70.8))
# Place(lat=95, ...) -> ValidationError, latitude out of rangeLatitude clamps to -90..90 and Longitude to -180..180, which catches the single most common geo bug: passing the pair in the wrong order. Coordinate accepts a two-item tuple or a 'lat,lon' string and gives you a named pair back.
Validate an IANA timezone nametimezone-name
from pydantic import BaseModel
from pydantic_extra_types.timezone_name import TimeZoneName, TimeZoneNameSettings
class Location(BaseModel):
city: str
timezone: TimeZoneName
class TZNonStrict(TimeZoneName, metaclass=TimeZoneNameSettings, strict=False):
pass
print(Location(city='New York', timezone='America/New_York').timezone)
print(TZNonStrict('america/new_york')) # case-insensitive matchThe allowed set comes from zoneinfo.available_timezones(), which is empty on slim images with no system tz database; install tzdata or every value fails. Subclass with strict=False through the metaclass if you need to accept lowercase input.
Validate ISO currency and language codescurrency-and-language
from pydantic import BaseModel
from pydantic_extra_types.currency_code import ISO4217, Currency
from pydantic_extra_types.language_code import LanguageAlpha2, LanguageName
class Invoice(BaseModel):
total_currency: ISO4217 # every ISO 4217 code, funds included
display_currency: Currency # ISO 4217 minus the special non-currency codes
locale: LanguageAlpha2
Invoice(total_currency='AED', display_currency='USD', locale='de')ISO4217 and Currency are not the same list: Currency filters out the placeholder codes such as XXX and the fund codes. LanguageAlpha2 is the two-letter set, ISO639_3 the three-letter one, and LanguageName the English name. All four need the pycountry extra.
Accept a unix timestamp and get a datetimeepoch-timestamp
from pydantic import BaseModel
from pydantic_extra_types import epoch
class Event(BaseModel):
at: epoch.Integer # whole seconds
precise: epoch.Number # float seconds
e = Event(at=1_752_000_000, precise=1_752_000_000.25)
print(e.at) # datetime, UTC
print(e.model_dump()) # numbers again, not ISO stringsThese are seconds since the epoch, not milliseconds. Feed a JavaScript Date.now() value and the result is a date far in the future or a range error. Serialization round-trips back to a number, which is what makes it useful as a wire type.
Require a specific UUID versionuuid-version-constraint
from uuid import UUID
from pydantic import BaseModel
from pydantic_extra_types.uuid_types import UUID6, UUID7, UUID8
class Document(BaseModel):
id: UUID7
Document(id='01930000-0000-7000-8000-000000000000')
# Document(id=UUID(int=0)) -> ValidationError: expected version 7These are Annotated[uuid.UUID, ...] constraints, so they only check the version nibble of a value you already have. They generate nothing; use the uuid-utils package or your database if you need to mint a v7.
Validate ISBN, ISIN, and MAC address stringsidentifier-check-digits
from pydantic import BaseModel
from pydantic_extra_types.isbn import ISBN
from pydantic_extra_types.isin import ISIN
from pydantic_extra_types.mac_address import MacAddress
class Record(BaseModel):
book: ISBN
security: ISIN
nic: MacAddress
r = Record(book='8537809667', security='US0378331005', nic='00:00:5e:00:53:01')
print(r.book) # 9788537809662, normalised to ISBN-13ISBN accepts ISBN-10 and converts to ISBN-13 on the way in, so the stored value is not always the string that was submitted. None of these three needs an extra, which makes them the cheapest reason to add the package.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| python-stdnum | PyPI | You mainly need identifier validation (IBAN, ISIN, VAT numbers, national IDs, hundreds of country-specific formats) and can call a validator function yourself instead of using an annotation. |
| pycountry | PyPI | You only want ISO country, language, currency, and subdivision lookups, without a pydantic layer on top. |
| phonenumbers | PyPI | Phone numbers are your only case; using the library directly in a field_validator gives you access to carrier, timezone, and geocoding data these wrappers do not expose. |