pycountry review
pycountry packages Debian's iso-codes data behind Python collections for current and historic countries, subdivisions, languages, currencies, and scripts. Records support exact get calls, case-insensitive lookup, iteration, and fuzzy searches where implemented; gettext catalogs provide translated ISO names offline. Version 26.2.16 updates the source data to iso-codes 4.20.1, adds Python 3.13 and 3.14 support, matches initials such as UK in country fuzzy search, and repairs subdivision indexes after runtime add or remove operations. Our install confirmed a typed, pure-Python package with no direct dependencies.
pycountry is a practical offline source when an application genuinely needs several ISO tables or their translations. For a country dropdown, compare Babel's display names; for two-letter validation alone, the measured 23 MB footprint is hard to defend.
We installed it
| Install | ✓ · 0.5s | 1 package on disk · 23 MB |
| Import | ✓ | import pycountry in 0.27s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pycountry install cleanly?
Yes. In a fresh container with an empty cache, pip install pycountry finished in 0.5s, leaving 1 package and 23 MB on disk. pip-audit reported no known vulnerabilities.
What does pycountry need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import pycountry succeeded in 0.27s, and the package ships py.typed for type checkers.
pycountry or iso3166: which should you use?
iso3166: Use it when the requirement stops at current ISO 3166-1 country records. pycountry is a practical offline source when an application genuinely needs several ISO tables or their translations.
When should you not use pycountry?
Only ISO 3166-1 country codes are needed. A smaller package such as iso3166 avoids installing the other standards and translation catalogs
Use it if
- An application must normalize ISO country, language, currency, script, or subdivision codes without a network service
- Address forms need ISO 3166-2 records and parent links grouped by country code
- Historic ISO 3166-3 identifiers must remain resolvable in archived data
- Translated ISO names should come from bundled gettext catalogs rather than an API
- Only ISO 3166-1 country codes are needed. A smaller package such as iso3166 avoids installing the other standards and translation catalogs
- The product needs capitals, continents, calling codes, coordinates, or currency minor units. Those fields are outside pycountry's ISO tables
- User-facing country names should follow CLDR conventions. ISO political names can be awkward in menus, while Babel provides locale display names
- Data corrections must be accepted directly by the Python project. The README says pycountry mirrors Debian iso-codes and rejects local changes to the underlying standard data
- LGPLv2 distribution terms do not pass your legal review, especially for a bundled executable or appliance
Setup reality
We installed pycountry 26.2.16 in a fresh Python 3.12 Bookworm container. pip completed in 0.5 seconds and left one package occupying 23 MB. There are zero direct dependencies and no compiled extensions. The package requires Python 3.10 or newer, carries py.typed, and reports LGPLv2 in the measured metadata. Importing pycountry worked in 0.27 seconds. pip-audit reported no known vulnerabilities.
Most of the footprint is data and translations; there is no supported extra that installs only countries or omits locales. That matters in small container images and function layers even though installation itself is simple. The records are local snapshots from Debian's iso-codes project. Version 26.2.16 uses iso-codes 4.20.1, so an ISO or Debian change after that snapshot needs a future pycountry release or an application-owned entry.
Miss behavior depends on the method. get(alpha_2='ZZ') returns None, while lookup('ZZ') and an unsuccessful fuzzy search raise LookupError. Optional attributes exist only when the standard supplies them. Read official_name, common_name, and some language codes with getattr(record, name, fallback) instead of assuming every record shares one schema. Fuzzy search returns a ranked list and can include subdivisions while resolving a country-style query.
Runtime add_entry and remove_entry calls mutate the process-level database. Version 26.2.16 fixes a bug where subdivision mutations broke the country_code index, but startup-only changes are still easier to reason about than per-request edits. For translations, select the correct gettext domain and call the translation object's gettext method; installing a process-global translation alias can leak one user's locale into another request.
Patterns
Resolve a country by known code lookup-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')Use a field-specific get when the input format is known; it avoids ambiguity and returns None on a miss.
Handle exact lookup misses handle-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, while lookup raises LookupError. Keep those paths distinct in request validation.
Read an optional record field optional-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 BanglaISO records do not all contain official_name or common_name, so getattr with a fallback is safer.
Build a code validation set validate-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()Create the set once and reuse it. Fuzzy search is intended for discovery rather than validation.
Suggest countries from a name fuzzy-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']search_fuzzy returns a ranked list and raises LookupError when no candidate exists. Confirm the selected record with the user.
List subdivisions for a country subdivisions-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'))) # 57A country_code query returns several records, whereas a unique code query returns one subdivision.
Follow a subdivision parent subdivision-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) # FranceTop-level subdivisions have no parent. Check parent_code before dereferencing parent.
Read a currency code currency-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)) # 178These records do not include decimal-place rules; use locale or payment-provider data for money formatting.
Translate an ISO name translate-names
import gettext
import pycountry
german = gettext.translation(
'iso3166-1', pycountry.LOCALES_DIR, languages=['de']
)
german.install()
print(_('Germany')) # DeutschlandKeep the gettext translation object scoped in web applications instead of installing a process-global translation alias.
Convert a record to a mapping cast-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", ...}Optional ISO fields produce different key sets across records, so normalize the response schema for an API.
Add an application-specific entry custom-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')Database mutation is process-wide. Apply custom entries once during startup and reset shared state in tests.
Look up a withdrawn country historic-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) # SUHHHistoric codes live in historic_countries and include fields such as alpha_4 and withdrawal_date.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| iso3166 | PyPI | Use it when the requirement stops at current ISO 3166-1 country records. |
| Babel | PyPI | Use it for CLDR display names, locale rules, and currency formatting in user interfaces. |
| country-converter | PyPI | Use it for messy-name conversion and analytical groupings such as continents or organizations. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

