This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
GeoIP2-python is MaxMind's official Python client library for:
- GeoIP2/GeoLite2 Web Services: Country, City, and Insights endpoints
- GeoIP2/GeoLite2 Databases: Local MMDB file reading for various database types (City, Country, ASN, Anonymous IP, Anonymous Plus, ISP, etc.)
The library provides both web service clients (sync and async) and database readers that return strongly-typed model objects containing geographic, ISP, anonymizer, and other IP-related data.
Key Technologies:
- Python 3.10+ (type hints throughout, uses modern Python features)
- MaxMind DB Reader for binary database files
- Requests library for sync web service client
- aiohttp for async web service client
- pytest for testing
- ruff for linting and formatting
- mypy for static type checking
- uv for dependency management and building
src/geoip2/
├── models.py # Response models (City, Insights, AnonymousIP, etc.)
├── records.py # Data records (City, Location, Traits, etc.)
├── errors.py # Custom exceptions for error handling
├── database.py # Local MMDB file reader
├── webservice.py # HTTP clients (sync Client and async AsyncClient)
├── _internal.py # Internal base classes and utilities
└── types.py # Type definitions
Models (in models.py) are top-level responses returned by database lookups or web service calls:
Country- base model with country/continent dataCityextendsCountry- adds city, location, postal, subdivisionsInsightsextendsCity- adds additional web service fields (web service only)EnterpriseextendsCity- adds enterprise-specific fieldsAnonymousIP- anonymous IP lookup resultsAnonymousPlusextendsAnonymousIP- adds additional anonymizer fieldsASN,ConnectionType,Domain,ISP- specialized lookup models
Records (in records.py) are contained within models and represent specific data components:
PlaceRecord- abstract base withnamesdict and locale handlingCity,Continent,Country,RepresentedCountry,Subdivision- geographic recordsLocation,Postal,Traits,MaxMind- additional data records
Models and records use keyword-only arguments (except for required positional parameters):
def __init__(
self,
locales: Sequence[str] | None, # positional for records
*,
continent: dict[str, Any] | None = None,
country: dict[str, Any] | None = None,
# ... other keyword-only parameters
**_: Any, # ignore unknown keys
) -> None:Key points:
- Use
*to enforce keyword-only arguments - Accept
**_: Anyto ignore unknown keys from the API - Use
| None = Nonefor optional parameters - Boolean fields default to
Falseif not present
All model and record classes inherit from Model (in _internal.py) which provides to_dict():
def to_dict(self) -> dict[str, Any]:
# Returns a dict suitable for JSON serialization
# - Skips None values and False booleans
# - Recursively calls to_dict() on nested objects
# - Handles lists/tuples of objects
# - Converts network and ip_address to stringsThe to_dict() method replaced the old raw attribute in version 5.0.0.
Records with names use PlaceRecord base class:
namesdict contains locale code → name mappingsnameproperty returns the first available name based on locale preference- Default locale is
["en"]if not specified - Locales are passed down from models to records
For performance reasons, network and ip_address are properties rather than attributes:
@property
def network(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None:
# Lazy calculation and caching of network from ip_address and prefix_lenSome models are only used by web services and do not need MaxMind DB support:
Web Service Only Models:
Insights- extends City but used only for web service- Simpler implementation without database parsing logic
Database-Supported Models:
- Models used by both web services and database files
- Must handle MaxMind DB format data structures
- Examples:
City,Country,AnonymousIP,AnonymousPlus,ASN,ISP
# Install dependencies using uv
uv sync --all-groups
# Run all tests
uv run pytest
# Run specific test file
uv run pytest tests/models_test.py
# Run specific test class or method
uv run pytest tests/models_test.py::TestModels::test_insights_full
# Run tests with coverage
uv run pytest --cov=geoip2 --cov-report=html# Run all linting checks (mypy, ruff check, ruff format check)
uv run tox -e lint
# Run mypy type checking
uv run mypy src tests
# Run ruff linting
uv run ruff check
# Auto-fix ruff issues
uv run ruff check --fix
# Check formatting
uv run ruff format --check --diff .
# Apply formatting
uv run ruff format .# Run tests on all supported Python versions
uv run tox
# Run on specific Python version
uv run tox -e 3.11
# Run lint environment
uv run tox -e lintTests are organized by component:
tests/database_test.py- Database reader teststests/models_test.py- Response model teststests/webservice_test.py- Web service client tests
When adding new fields to models:
- Update the test method to include the new field in the
rawdict - Add assertions to verify the field is properly populated
- Test both presence and absence of the field (null handling)
- Verify
to_dict()serialization includes the field correctly
Example:
def test_anonymous_plus_full(self) -> None:
model = geoip2.models.AnonymousPlus(
"1.2.3.4",
anonymizer_confidence=99,
network_last_seen=datetime.date(2025, 4, 14),
provider_name="FooBar VPN",
is_anonymous=True,
is_anonymous_vpn=True,
# ... other fields
)
assert model.anonymizer_confidence == 99
assert model.network_last_seen == datetime.date(2025, 4, 14)
assert model.provider_name == "FooBar VPN"-
Add the parameter to
__init__with proper type hints:def __init__( self, # ... existing params *, field_name: int | None = None, # new field # ... other params ) -> None:
-
Assign the field in the constructor:
self.field_name = field_name
-
Add class-level type annotation with docstring:
field_name: int | None """Description of the field, its source, and availability."""
-
Update
to_dict()if special handling needed (usually automatic via_internal.Model) -
Update tests to include the new field in test data and assertions
-
Update HISTORY.rst with the change (see CHANGELOG Format below)
When creating a new model class:
- Determine if web service only or database-supported
- Follow the pattern from existing similar models
- Extend the appropriate base class (e.g.,
Country,City,SimpleModel) - Use type hints for all attributes
- Use keyword-only arguments with
*separator - Accept
**_: Anyto ignore unknown API keys - Provide comprehensive docstrings for all attributes
- Add corresponding tests with full coverage
When a field returns a date string from the API (e.g., "2025-04-14"):
-
Parse it to
datetime.datein the constructor:import datetime self.network_last_seen = ( datetime.date.fromisoformat(network_last_seen) if network_last_seen else None )
-
Annotate as
datetime.date | None:network_last_seen: datetime.date | None
-
In
to_dict(), dates are automatically converted to ISO format strings by the base class
When deprecating fields:
-
Add deprecation to docstring with version and alternative:
metro_code: int | None """The metro code of the location. .. deprecated:: 5.0.0 The code values are no longer being maintained. """
-
Keep deprecated fields functional - don't break existing code
-
Update HISTORY.rst with deprecation notices
-
Document alternatives in the deprecation message
Always update HISTORY.rst for user-facing changes.
Important: Do not add a date to changelog entries until release time. Version numbers are added but without dates.
Format:
5.2.0
++++++++++++++++++
* IMPORTANT: Python 3.10 or greater is required. If you are using an older
version, please use an earlier release.
* A new ``field_name`` property has been added to ``geoip2.models.ModelName``.
This field provides information about...
* The ``old_field`` property in ``geoip2.models.ModelName`` has been deprecated.
Please use ``new_field`` instead.Using wrong type hints can cause mypy errors or allow invalid data.
Solution: Follow these patterns:
- Optional values:
Type | None(e.g.,int | None,str | None) - Non-null booleans:
bool(default toFalsein constructor if not present) - Sequences:
Sequence[str]for parameters,list[T]for internal lists - IP addresses:
IPAddresstype alias (fromgeoip2.types) - IP objects:
IPv4Address | IPv6Addressfromipaddressmodule
New fields not appearing in serialized output.
Solution: The to_dict() method in _internal.Model automatically handles most cases:
- Non-None values are included
- False booleans are excluded
- Empty dicts/lists are excluded
- Nested objects with
to_dict()are recursively serialized
If you need custom serialization, override to_dict() carefully.
Tests fail because fixtures don't include new fields.
Solution: Update all related tests:
- Add field to constructor calls in tests
- Add assertions for the new field
- Test null case if field is optional
- Verify
to_dict()serialization
Breaking changes when adding required parameters.
Solution:
- Use keyword-only arguments (after
*) for all optional parameters - Only add new parameters as optional with defaults
- Never add required positional parameters to existing constructors
- ruff enforces all style rules (configured in
pyproject.toml) - Type hints required for all functions and class attributes
- Docstrings required for all public classes, methods, and attributes (Google style)
- Line length: 88 characters (Black-compatible)
- No unused imports or variables
- Use modern Python features (3.10+ type union syntax:
X | Yinstead ofUnion[X, Y])
# Install uv if not already installed
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install all dependencies including dev and lint groups
uv sync --all-groups# Format code
uv run ruff format .
# Check linting
uv run ruff check --fix
# Type check
uv run mypy src tests
# Run tests
uv run pytest
# Or run everything via tox
uv run tox- Python 3.10+ required (as of version 5.2.0)
- Uses modern Python features (match statements, structural pattern matching,
X | Yunion syntax) - Target compatibility: Python 3.10-3.14