django-countries icon indicating copy to clipboard operation
django-countries copied to clipboard

Can `Country.code` be `None` in v8.x?

Open jodal opened this issue 2 months ago • 2 comments

After the addition of fields.pyi in d6968e3390a15008fa0ecda322b9454379e7ae6b, Country.code can now be None.

Looking at the actual implementation in Country.__init__(), which accepts code: str and then does:

self.code = self.countries.alpha2(code) or code

...it doesn't look like Country.code can actually ever be None?

Reason I'm asking is that we're running into ~20 typing warnings in our codebase when upgrading to django-countries 8.x because we've been under the belief that if we have a Country, we always have a code too.

Is this assumption no longer correct, or is the new Country.code type too wide?

jodal avatar Nov 15 '25 10:11 jodal

Thanks for the trigger, there's reasoning here with recent changes around making the field nullable (it was either a call of backwards compatibility by always return a country object rather than None, so I decided that maybe returning a None-able country made more sense).

It made typing "interesting" to try and make pyright and mypy do the right thing with overloads depending on the nullable argument if it returned None. I gave up and just did the somewhat cheat way but the argument is still open for just not returning a country object at all in those cases, since the nullable case wasn't a thing before. Still makes typing more likely to choke when you can't rely on a country wheras you could before (unless you can fix the overload!). I'll have another think and look at it soon

SmileyChris avatar Nov 17 '25 07:11 SmileyChris

I don't think the nullability should be defined in a field variable.

If a CountryField has null=True, that should be defined as follows:

from django.db.models import Field
from django.db.models.base import Model
from django_countries.fields import Country, CountryField

class SomeModel(Model):
    country: Field[Country | None, Country | None] = CountryField(null=True, verbose_name=_('Country'))

Which means, SomeModel can have None as a country field, or an actual Country. Same goes for setting the field: either you assign an actual Country or None.

karolyi avatar Dec 01 '25 19:12 karolyi