Skip to content

Reference

API Reference

Comprehensive reference for the library types, generated with mkdocstrings.

DNI

Bases: PydanticStringID

Validated Spanish DNI string.

A DNI comprises 8 digits followed by a control letter. The letter is derived from the numeric part modulo 23 and mapped to the sequence TRWAGMYFPDXBNJZSQVHLCKE.

Source code in src/spanish_nif/dni.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class DNI(PydanticStringID):
    """Validated Spanish DNI string.

    A DNI comprises 8 digits followed by a control letter. The letter is derived
    from the numeric part modulo 23 and mapped to the sequence
    ``TRWAGMYFPDXBNJZSQVHLCKE``.
    """

    _pattern = re.compile(r"^(\d{8})([A-Z])$")
    _control_letters = "TRWAGMYFPDXBNJZSQVHLCKE"
    json_pattern = r"^\d{8}[A-Z]$"
    json_examples = ["12345678Z"]
    json_description = (
        "Spanish Documento Nacional de Identidad: 8 digits plus control letter."
    )

    def __new__(cls, value: str) -> "DNI":
        return super().__new__(cls, cls._normalize(value))

    @classmethod
    def random(cls, rng: random.Random | None = None) -> "DNI":
        """Return a random, valid DNI instance."""

        generator = rng if rng is not None else random.Random()
        number = generator.randint(0, 9_999_9999)
        digits = f"{number:08d}"
        letter = cls._control_letters[number % 23]
        return cls(f"{digits}{letter}")

    @classmethod
    def _normalize(cls, value: Any) -> str:
        normalized = str(value).upper()
        match = cls._pattern.fullmatch(normalized)
        if not match:
            raise InvalidDNI(
                "DNI must consist of 8 digits followed by an uppercase letter"
            )

        digits, letter = match.groups()
        expected_letter = cls._control_letters[int(digits) % 23]
        if letter != expected_letter:
            raise InvalidDNI(f"Invalid DNI control letter; expected {expected_letter}")
        return normalized

    @property
    def digits(self) -> str:
        """Return the zero-padded 8-digit numeric part."""

        return self[:8]

    @property
    def number(self) -> int:
        """Return the numeric portion of the DNI as an integer."""

        return int(self[:8])

    @property
    def letter(self) -> str:
        """Return the control letter."""

        return self[8]

    @classmethod
    def is_valid(cls, value: str) -> bool:
        """Return ``True`` when *value* is a valid DNI."""

        try:
            cls(value)
        except InvalidDNI:
            return False
        return True

digits property

Return the zero-padded 8-digit numeric part.

letter property

Return the control letter.

number property

Return the numeric portion of the DNI as an integer.

is_valid(value) classmethod

Return True when value is a valid DNI.

Source code in src/spanish_nif/dni.py
78
79
80
81
82
83
84
85
86
@classmethod
def is_valid(cls, value: str) -> bool:
    """Return ``True`` when *value* is a valid DNI."""

    try:
        cls(value)
    except InvalidDNI:
        return False
    return True

random(rng=None) classmethod

Return a random, valid DNI instance.

Source code in src/spanish_nif/dni.py
35
36
37
38
39
40
41
42
43
@classmethod
def random(cls, rng: random.Random | None = None) -> "DNI":
    """Return a random, valid DNI instance."""

    generator = rng if rng is not None else random.Random()
    number = generator.randint(0, 9_999_9999)
    digits = f"{number:08d}"
    letter = cls._control_letters[number % 23]
    return cls(f"{digits}{letter}")

Bases: InvalidIdentification

Raised when a DNI does not comply with format or control-letter rules.

Source code in src/spanish_nif/dni.py
12
13
class InvalidDNI(InvalidIdentification):
    """Raised when a DNI does not comply with format or control-letter rules."""

NIE

Bases: PydanticStringID

Validated Spanish NIE string.

Source code in src/spanish_nif/nie.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class NIE(PydanticStringID):
    """Validated Spanish NIE string."""

    _pattern = re.compile(r"^([XYZ])(\d{7})([A-Z])$")
    _prefix_map = {"X": "0", "Y": "1", "Z": "2"}
    _control_letters = "TRWAGMYFPDXBNJZSQVHLCKE"
    json_pattern = r"^[XYZ]\d{7}[A-Z]$"
    json_examples = ["X1234567L"]
    json_description = (
        "Número de Identidad de Extranjero: X/Y/Z + 7 digits + control letter."
    )

    def __new__(cls, value: str) -> "NIE":
        return super().__new__(cls, cls._normalize(value))

    @classmethod
    def random(cls, rng: random.Random | None = None) -> "NIE":
        """Return a random, valid NIE instance."""

        generator = rng if rng is not None else random.Random()
        prefix = generator.choice(tuple(cls._prefix_map))
        number = generator.randint(0, 9_999_999)
        digits = f"{number:07d}"
        numeric_value = int(cls._prefix_map[prefix] + digits)
        letter = cls._control_letters[numeric_value % 23]
        return cls(f"{prefix}{digits}{letter}")

    @classmethod
    def _normalize(cls, value: Any) -> str:
        normalized = str(value).upper()
        match = cls._pattern.fullmatch(normalized)
        if not match:
            raise InvalidNIE(
                "NIE must start with X, Y or Z followed by 7 digits and a control letter"
            )

        prefix, digits, letter = match.groups()
        numeric_value = int(cls._prefix_map[prefix] + digits)
        expected_letter = cls._control_letters[numeric_value % 23]
        if letter != expected_letter:
            raise InvalidNIE(f"Invalid NIE control letter; expected {expected_letter}")
        return normalized

    @property
    def prefix(self) -> str:
        """Return the leading status letter (X, Y or Z)."""

        return self[0]

    @property
    def digits(self) -> str:
        """Return the 7-digit numeric part."""

        return self[1:8]

    @property
    def number(self) -> int:
        """Return the numeric representation used in the control-letter computation."""

        return int(self._prefix_map[self.prefix] + self.digits)

    @property
    def letter(self) -> str:
        """Return the control letter."""

        return self[8]

    @classmethod
    def is_valid(cls, value: str) -> bool:
        """Return ``True`` when *value* is a valid NIE."""

        try:
            cls(value)
        except InvalidNIE:
            return False
        return True

digits property

Return the 7-digit numeric part.

letter property

Return the control letter.

number property

Return the numeric representation used in the control-letter computation.

prefix property

Return the leading status letter (X, Y or Z).

is_valid(value) classmethod

Return True when value is a valid NIE.

Source code in src/spanish_nif/nie.py
83
84
85
86
87
88
89
90
91
@classmethod
def is_valid(cls, value: str) -> bool:
    """Return ``True`` when *value* is a valid NIE."""

    try:
        cls(value)
    except InvalidNIE:
        return False
    return True

random(rng=None) classmethod

Return a random, valid NIE instance.

Source code in src/spanish_nif/nie.py
31
32
33
34
35
36
37
38
39
40
41
@classmethod
def random(cls, rng: random.Random | None = None) -> "NIE":
    """Return a random, valid NIE instance."""

    generator = rng if rng is not None else random.Random()
    prefix = generator.choice(tuple(cls._prefix_map))
    number = generator.randint(0, 9_999_999)
    digits = f"{number:07d}"
    numeric_value = int(cls._prefix_map[prefix] + digits)
    letter = cls._control_letters[numeric_value % 23]
    return cls(f"{prefix}{digits}{letter}")

Bases: InvalidIdentification

Raised when an NIE does not comply with format or control-letter rules.

Source code in src/spanish_nif/nie.py
12
13
class InvalidNIE(InvalidIdentification):
    """Raised when an NIE does not comply with format or control-letter rules."""

NIF

Bases: PydanticStringID

Validated Spanish NIF string.

This covers natural-person identifiers: standard DNI numbers, NIE numbers (foreign residents), and K/L/M prefixes.

Source code in src/spanish_nif/nif.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
class NIF(PydanticStringID):
    """Validated Spanish NIF string.

    This covers natural-person identifiers: standard DNI numbers, NIE numbers
    (foreign residents), and K/L/M prefixes.
    """

    json_pattern = r"^(?:\d{8}|[KLMXYZ]\d{7})[A-Z]$"
    json_examples = ["12345678Z", "K0867756N", "X1234567L"]
    json_description = (
        "Número de Identificación Fiscal for natural persons. Accepts DNI, NIE, "
        "and K/L/M prefixes."
    )

    _klm_pattern = re.compile(r"^([KLM])(\d{7})([A-Z])$")

    def __new__(cls, value: Any) -> "NIF":
        return super().__new__(cls, value)

    @classmethod
    def random(
        cls, rng: random.Random | None = None, *, variant: str | None = None
    ) -> "NIF":
        """Return a random, valid NIF instance.

        Args:
            rng: Optional pseudo-random generator to use. Defaults to the
                module-level :mod:`random` functions.
            variant: Optional variant selector: ``"dni"``, ``"nie"`` or
                ``"klm"``. When omitted a variant is chosen uniformly.
        """

        generator = rng if rng is not None else random.Random()
        chosen_variant = (variant or generator.choice(("dni", "nie", "klm"))).lower()

        if chosen_variant == "dni":
            return cls(str(DNI.random(rng=generator)))

        if chosen_variant == "nie":
            return cls(str(NIE.random(rng=generator)))

        if chosen_variant == "klm":
            prefix = generator.choice(("K", "L", "M"))
            digits = f"{generator.randint(0, 9_999_999):07d}"
            letter = DNI._control_letters[int(digits) % 23]
            return cls(f"{prefix}{digits}{letter}")

        raise ValueError("variant must be one of 'dni', 'nie', or 'klm' if provided")

    @classmethod
    def _normalize(cls, value: Any) -> str:
        normalized = str(value).upper()
        if not normalized:
            raise InvalidNIF("NIF cannot be empty")

        # NIE handling (X/Y/Z prefixes)
        try:
            return str(NIE(normalized))
        except InvalidNIE:
            pass

        # Standard DNI (8 digits)
        try:
            return str(DNI(normalized))
        except InvalidDNI:
            pass

        # K/L/M prefixes (7 digits + letter)
        match = cls._klm_pattern.fullmatch(normalized)
        if match:
            _, digits, letter = match.groups()
            expected = DNI._control_letters[int(digits) % 23]
            if letter != expected:
                raise InvalidNIF(f"Invalid NIF control letter; expected {expected}")
            return normalized

        raise InvalidNIF("NIF must correspond to a valid DNI, NIE, or K/L/M format")

    @property
    def variant(self) -> str:
        """Return the identifier variant: ``dni``, ``nie`` or ``klm``."""

        prefix = self[0]
        if prefix in "XYZ":
            return "nie"
        if prefix in "KLM":
            return "klm"
        return "dni"

    @property
    def digits(self) -> str:
        """Return the digits used to compute the control letter."""

        if self.variant == "nie":
            numeric = NIE._prefix_map[self[0]] + self[1:8]
            return numeric
        if self.variant == "klm":
            return self[1:8]
        return self[:8]

    @property
    def number(self) -> int:
        """Return the numeric component that drives the control letter."""

        return int(self.digits)

    @property
    def letter(self) -> str:
        """Return the control letter."""

        return self[-1]

    @classmethod
    def is_valid(cls, value: str) -> bool:
        """Return ``True`` when *value* is a valid NIF."""

        try:
            cls(value)
        except InvalidNIF:
            return False
        return True

digits property

Return the digits used to compute the control letter.

letter property

Return the control letter.

number property

Return the numeric component that drives the control letter.

variant property

Return the identifier variant: dni, nie or klm.

is_valid(value) classmethod

Return True when value is a valid NIF.

Source code in src/spanish_nif/nif.py
130
131
132
133
134
135
136
137
138
@classmethod
def is_valid(cls, value: str) -> bool:
    """Return ``True`` when *value* is a valid NIF."""

    try:
        cls(value)
    except InvalidNIF:
        return False
    return True

random(rng=None, *, variant=None) classmethod

Return a random, valid NIF instance.

Parameters:

Name Type Description Default
rng Random | None

Optional pseudo-random generator to use. Defaults to the module-level :mod:random functions.

None
variant str | None

Optional variant selector: "dni", "nie" or "klm". When omitted a variant is chosen uniformly.

None
Source code in src/spanish_nif/nif.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@classmethod
def random(
    cls, rng: random.Random | None = None, *, variant: str | None = None
) -> "NIF":
    """Return a random, valid NIF instance.

    Args:
        rng: Optional pseudo-random generator to use. Defaults to the
            module-level :mod:`random` functions.
        variant: Optional variant selector: ``"dni"``, ``"nie"`` or
            ``"klm"``. When omitted a variant is chosen uniformly.
    """

    generator = rng if rng is not None else random.Random()
    chosen_variant = (variant or generator.choice(("dni", "nie", "klm"))).lower()

    if chosen_variant == "dni":
        return cls(str(DNI.random(rng=generator)))

    if chosen_variant == "nie":
        return cls(str(NIE.random(rng=generator)))

    if chosen_variant == "klm":
        prefix = generator.choice(("K", "L", "M"))
        digits = f"{generator.randint(0, 9_999_999):07d}"
        letter = DNI._control_letters[int(digits) % 23]
        return cls(f"{prefix}{digits}{letter}")

    raise ValueError("variant must be one of 'dni', 'nie', or 'klm' if provided")

Bases: InvalidIdentification

Raised when a NIF does not comply with format or control-letter rules.

Source code in src/spanish_nif/nif.py
14
15
class InvalidNIF(InvalidIdentification):
    """Raised when a NIF does not comply with format or control-letter rules."""