MISSING Constant
The MISSING
constant is similar to None
, but with a key difference. While None
has the type NoneType
and can only be assigned to fields of type T | None
, the MISSING
constant has the type Any
and can be assigned to fields of any type.
Motivation
The MISSING
constant addresses a common issue when working with optional fields in configurations. Consider the following example:
import nshconfig as C
from typing import Annotated
# Without MISSING:
class MyConfigWithoutMissing(C.Config):
age: int
age_str: str | None = None
def __post_init__(self):
if self.age_str is None:
self.age_str = str(self.age)
config = MyConfigWithoutMissing(age=10)
age_str_lower = config.age_str.lower()
# ^ The above line is valid code, but the type-checker will complain
# because `age_str` could be `None`.
In the above code, the type-checker will raise a complaint because age_str
could be None
. This is where the MISSING
constant comes in handy:
Using MISSING with AllowMissing
There are two syntaxes for using the MISSING
constant with the AllowMissing
feature:
Syntax 1: Using Annotated
# With MISSING (Annotated syntax):
class MyConfigWithMissing(C.Config):
age: int
age_str: Annotated[str, C.AllowMissing] = C.MISSING
def __post_init__(self):
if self.age_str is C.MISSING:
self.age_str = str(self.age)
config = MyConfigWithMissing(age=10)
age_str_lower = config.age_str.lower()
# ^ No more type-checker complaints!
Syntax 2: Direct AllowMissing Type
A more concise syntax is available using AllowMissing
directly as a generic type:
# With MISSING (direct AllowMissing syntax):
class MyConfigWithMissing(C.Config):
age: int
age_str: C.AllowMissing[str] = C.MISSING
def __post_init__(self):
if self.age_str is C.MISSING:
self.age_str = str(self.age)
config = MyConfigWithMissing(age=10)
age_str_lower = config.age_str.lower()
# ^ No type-checker complaints with this syntax either!
This second syntax is more concise and follows modern Python typing patterns.
By using either syntax with the MISSING
constant, you can indicate that a field is not set during construction, and the type-checker will not raise any complaints.
Validating No Missing Values
If you want to ensure that your configuration doesn’t contain any MISSING
values after initialization, you can call the model_validate_no_missing()
method:
class MyConfig(C.Config):
required_field: int
optional_field: C.AllowMissing[str] = C.MISSING # Using the direct syntax
def __post_init__(self):
# Set default values, etc.
if self.optional_field is C.MISSING:
self.optional_field = "default value"
# Ensure no MISSING values remain
self.model_validate_no_missing()
# This will work fine:
config1 = MyConfig(required_field=10)
# This would raise a validation error if model_validate_no_missing()
# wasn't setting a default value in __post_init__:
config1.optional_field # "default value"
The model_validate_no_missing()
method is useful when you want to ensure all fields have proper values before using your configuration.
Using AllowMissing with Complex Types
The AllowMissing
feature works seamlessly with complex types like lists, dictionaries, and nested configurations:
class NestedConfig(C.Config):
value: C.AllowMissing[str] = C.MISSING
class ParentConfig(C.Config):
name: str
nested: C.AllowMissing[NestedConfig] = C.MISSING
items: C.AllowMissing[list[int]] = C.MISSING
settings: C.AllowMissing[dict[str, float]] = C.MISSING
# Create with MISSING values
config = ParentConfig(name="test")
assert config.nested is C.MISSING
assert config.items is C.MISSING
assert config.settings is C.MISSING
# Assign values later
config.items = [1, 2, 3]
config.settings = {"scale": 1.5, "offset": 0.1}
This flexibility allows for dynamic configuration patterns where some values might not be available at initialization time.