mrkeyoor.com_
Fri 07 Aug 19:04 UTC
PyPIUtilsupdated 07 Aug 2026

pycountry

pycountry vendors Debian's iso-codes database and puts a small Python API in front of it. Import it and you get five lookup tables ready to query offline: pycountry.countries (249 ISO 3166-1 entries), pycountry.subdivisions (5046 ISO 3166-2 entries with parent links), pycountry.historic_countries (31 withdrawn ISO 3166-3 entries), pycountry.currencies (178 ISO 4217 entries), pycountry.languages (7923 ISO 639-3 entries) and pycountry.scripts (226 ISO 15924 entries). Each table supports get(field=value) for exact lookups, lookup(value) for a case-insensitive scan across every field, search_fuzzy(text) for approximate name matching, and plain iteration. It also ships the gettext catalogs from iso-codes, so you can render country and language names in dozens of locales without calling anything over the network. It is a data package with a query helper attached, not a geo or i18n framework.

Verdict

The default answer for offline ISO code lookups in Python, and the translation catalogs alone justify it for anything multilingual. Just budget for the 23 MB on disk and check that you actually need five standards, because if you only validate two-letter country codes you are paying a lot for a set membership test.

API stability5/5The get / lookup / search_fuzzy / iterate surface has not meaningfully moved in years. 26.2.16 dropped end-of-life Python 3.8 and 3.9 and added 3.13 and 3.14 support, but the calls you write today are the calls from a 2022 tutorial.
Docs3/5The README is a well-organized tour with real doctest output for every database, which covers most first-day questions. There is no hosted API reference, and the sharp edges are learned by hitting them: get() returning None versus lookup() raising LookupError, and AttributeError on optional fields such as official_name, are both absent from the prose.
Maintenance3/5Alive but unhurried. Releases land roughly once a year (22.3.5, 23.12.11, 24.6.1, 26.2.16), the last commit on main is dated 2026-02-18, and the tracker sits at 18 open issues. The stated no-data-changes policy means most incoming requests get closed rather than fixed, which keeps the queue short.
Ecosystem4/5The default ISO data dependency in Python, pulled in by address validation, billing and i18n code across the ecosystem, and packaged by every major distro. It loses a point because rivals cover parts of the job better: babel for display names, iso3166 for pure size.

Use it if

  • You need to validate or normalize ISO codes offline: turning 'de', 'DEU', '276' or 'Germany' into one canonical record with no network call and no API key
  • You are building an address or checkout form and want the ISO 3166-2 subdivision list per country, including the parent and parent_code links that let you render region then department
  • You need country or language names in a locale other than English: the gettext catalogs ship inside the package, so gettext.translation('iso3166-1', pycountry.LOCALES_DIR, languages=['de']) gives you Deutschland with no service dependency
  • You want one dependency-free package that covers 3166-1, 3166-2, 3166-3, 4217, 639-3 and 15924 rather than gluing four small libraries together
Skip it if

Setup reality

pip install pycountry needs Python 3.10 or newer, pulls in nothing else, and needs no compiler. The cost is disk and image size rather than build time: 7.67 MB of wheel expands to about 23 MB in site-packages, which is real money in a Lambda layer or a slim container, and there is no extras marker to install only the tables you use. Import is cheap because the JSON files load lazily on first access to each database, so the initial pycountry.subdivisions query pays a one-off cost of tens of milliseconds while 5046 records parse. The two access patterns behave differently on a miss and nothing warns you: get() returns None, lookup() raises LookupError. Optional fields are equally sharp, because a record only carries the keys the standard filled in, so country.official_name and language.common_name raise AttributeError on entries that lack them and getattr with a default is the only safe read. Translations need the gettext module and the right domain name (iso3166-1, iso3166-2, iso4217, iso639-3, iso15924) pointed at pycountry.LOCALES_DIR.

Patterns

Resolve a country from whatever code you were handedlookup-by-any-code

import pycountry

country = pycountry.countries.lookup('de')
print(country.alpha_2, country.alpha_3, country.numeric, country.name)
# DE DEU 276 Germany

# lookup() scans every field case-insensitively:
pycountry.countries.lookup('DEU')
pycountry.countries.lookup('276')
pycountry.countries.lookup('germany')

lookup() returns the first field match and stops, so a two-letter string that is also a valid alpha_3 prefix can surprise you. When you know which field you have, get(alpha_2=...) is both faster and unambiguous.

Tell the two miss behaviours aparthandle-unknown-code

import pycountry

print(pycountry.countries.get(alpha_2='XX'))   # None

try:
    pycountry.countries.lookup('Narnia')
except LookupError as exc:
    print(exc)  # Could not find a record for 'narnia'

get() returns None, lookup() raises LookupError. Code that switches between them without adjusting the error handling ships a silent None into the database or an uncaught exception into a request handler.

Read fields that only some records haveoptional-fields

import pycountry

aland = pycountry.countries.get(alpha_2='AX')
print(getattr(aland, 'official_name', aland.name))

bengali = pycountry.languages.get(alpha_2='bn')
print(bengali.name, getattr(bengali, 'common_name', None))
# Bengali Bangla

Records only carry the keys the standard populated, and missing attributes raise AttributeError rather than returning None. official_name, common_name and alpha_2 on languages are the three that bite most often, so read them with getattr and a default.

Validate a country code without paying for a full lookupvalidate-user-input

import pycountry
from functools import lru_cache

@lru_cache(maxsize=1)
def valid_alpha_2() -> frozenset[str]:
    return frozenset(c.alpha_2 for c in pycountry.countries)

def is_valid(code: str) -> bool:
    return code.upper() in valid_alpha_2()

Iterating the database materializes all 249 records once; after that the check is a set hit. Do not call search_fuzzy in a validation path, it is a scan over names plus unicode normalization and costs milliseconds per call.

Match a country from a loose user-typed namefuzzy-search

import pycountry

print(pycountry.countries.search_fuzzy('England'))
# [Country(alpha_2='GB', alpha_3='GBR', ... name='United Kingdom', ...)]

print([c.alpha_2 for c in pycountry.countries.search_fuzzy('Cote')])
# ['CI', 'FR', 'HN']

It returns a ranked list, never a single record, and it raises LookupError when nothing matches. Accents are normalized, so Cote finds Cote d'Ivoire, but the tail of the list is noise: take the first result only when you also show the user what you picked.

List the states or regions of one countrysubdivisions-for-country

import pycountry

for sub in sorted(pycountry.subdivisions.get(country_code='US'), key=lambda s: s.name):
    print(sub.code, sub.type, sub.name)

print(len(pycountry.subdivisions.get(country_code='US')))  # 57

get(country_code=...) returns a list while get(code=...) returns one record, from the same method name. The US count is 57 because ISO 3166-2 includes DC, outlying territories and Puerto Rico, so do not expect 50.

Walk from a subdivision up to its parent and countrysubdivision-hierarchy

import pycountry

fr_01 = pycountry.subdivisions.get(code='FR-01')
print(fr_01.name, fr_01.type)        # Ain Metropolitan department
print(fr_01.parent_code)             # FR-ARA
print(fr_01.parent.name)             # Auvergne-Rhone-Alpes
print(fr_01.country.name)            # France

parent_code is None for top-level subdivisions, and .parent is None with it, so guard before chaining. The hierarchy is only two deep in most countries but France and the UK go deeper, which is why hard-coding one level breaks on real address data.

Look up a currency and its numeric codecurrency-lookup

import pycountry

inr = pycountry.currencies.get(alpha_3='INR')
print(dict(inr))
# {'alpha_3': 'INR', 'name': 'Indian Rupee', 'numeric': '356'}

print(len(pycountry.currencies))  # 178

Three fields, that is all ISO 4217 gives here. There is no minor-unit count, so pycountry cannot tell you that JPY has zero decimal places and BHD has three. Money formatting needs babel or your payment provider's table.

Render country names in another languagetranslate-names

import gettext
import pycountry

german = gettext.translation(
    'iso3166-1', pycountry.LOCALES_DIR, languages=['de']
)
german.install()
print(_('Germany'))  # Deutschland

The domain names are iso3166-1, iso3166-2, iso4217, iso639-3 and iso15924, one catalog per standard. install() rebinds the global _ for the whole process, so in a web app prefer german.gettext(name) and keep the translation object scoped.

Serialize a record for JSON outputcast-to-dict

import json
import pycountry

country = pycountry.countries.lookup('de')
print(json.dumps(dict(country)))
# {"alpha_2": "DE", "alpha_3": "DEU", "flag": "\ud83c\udde9\ud83c\uddea", ...}

dict(record) only includes the keys that record actually has, so two countries in the same response can serialize with different key sets. Normalize to a fixed schema before returning it from an API or your clients will trip over the missing official_name.

Add a code the standard does not havecustom-entries

import pycountry

pycountry.countries.add_entry(
    alpha_2='XK', alpha_3='XXK', name='Kosovo', numeric='926'
)
print(pycountry.countries.get(alpha_2='XK'))
print(len(pycountry.countries))  # 250

pycountry.countries.remove_entry(alpha_2='XK')

This mutates a process-global singleton, so run it once at startup rather than per request, and remember your test suite shares that state. It is the sanctioned escape hatch because the maintainers refuse data changes upstream, Kosovo being the usual reason people need it.

Resolve a country code that no longer existshistoric-countries

import pycountry

ussr = pycountry.historic_countries.get(alpha_3='SUN')
print(ussr.name, ussr.withdrawal_date)
# USSR, Union of Soviet Socialist Republics 1992-08-30
print(ussr.alpha_4)  # SUHH

This is a separate ISO 3166-3 table of 31 records and pycountry.countries will not find any of them, so archival data needs both lookups. Historic entries carry alpha_4 and withdrawal_date, which current countries do not have.

Alternatives

PackageRegistryPick it when
iso3166PyPIYou only need ISO 3166-1 country codes and want a tiny package instead of 23 MB on disk
babelPyPIYou need CLDR display names, currency formatting and locale-aware output rather than raw ISO records
country_converterPyPIYou are reconciling messy country name variants across datasets and want regional groupings like EU, OECD and continent
pycountry-convertPyPIYou specifically want country to continent mapping layered on top of pycountry