Skip to content

Instantly share code, notes, and snippets.

@lmmx
Last active February 3, 2026 20:53
Show Gist options
  • Select an option

  • Save lmmx/0a7c215598ab39506e539e40239d5ac4 to your computer and use it in GitHub Desktop.

Select an option

Save lmmx/0a7c215598ab39506e539e40239d5ac4 to your computer and use it in GitHub Desktop.
Selecting between paths based on their existence on the filesystem using default validation at runtime
from pathlib import Path
from typing import Annotated
from pydantic import BaseModel, BeforeValidator, Field, FilePath
PathA = Annotated[FilePath, BeforeValidator(lambda _: Path("a.txt"))]
PathB = Annotated[FilePath, BeforeValidator(lambda _: Path("b.txt"))]
class MyModel(BaseModel):
my_path: PathA | PathB = Field(default="sentinel", validate_default=True)
print(" >> Setup: create only a.txt")
Path("a.txt").touch()
Path("b.txt").unlink(missing_ok=True)
m1 = MyModel()
print(f"Selected: {m1.my_path}") # Should print a.txt
print()
print(" >> Setup: create only b.txt")
Path("a.txt").unlink(missing_ok=True)
Path("b.txt").touch()
m2 = MyModel()
print(f"Selected: {m2.my_path}") # Should print b.txt
from pathlib import Path
from typing import Annotated
from pydantic import BaseModel, BeforeValidator, Field, FilePath
# Single-expression callable
Candidate = lambda p: Annotated[FilePath, BeforeValidator(lambda _: Path(p))]
class MyModel(BaseModel):
my_path: Candidate("a.txt") | Candidate("b.txt") = Field(
default=None, validate_default=True
)
print(" >> Setup: create only a.txt")
Path("a.txt").touch()
Path("b.txt").unlink(missing_ok=True)
m1 = MyModel()
print(f"Selected: {m1.my_path}") # a.txt
print()
print(" >> Setup: create only b.txt")
Path("a.txt").unlink(missing_ok=True)
Path("b.txt").touch()
m2 = MyModel()
print(f"Selected: {m2.my_path}") # b.txt
from pathlib import Path
from typing import Annotated
from pydantic import BaseModel, BeforeValidator, Field, FilePath
class Candidate:
"""FilePath that ignores input and resolves to a fixed path.
Union of Candidates acts as a fallback chain (first existing wins)."""
def __class_getitem__(cls, p: str | Path):
return Annotated[FilePath, BeforeValidator(lambda _: Path(p))]
class MyModel(BaseModel):
my_path: Candidate["a.txt"] | Candidate["b.txt"] = Field(
default=None, validate_default=True
)
print(" >> Setup: create only a.txt")
Path("a.txt").touch()
Path("b.txt").unlink(missing_ok=True)
m1 = MyModel()
print(f"Selected: {m1.my_path}") # a.txt
print()
print(" >> Setup: create only b.txt")
Path("a.txt").unlink(missing_ok=True)
Path("b.txt").touch()
m2 = MyModel()
print(f"Selected: {m2.my_path}") # b.txt
from pathlib import Path
from typing import Annotated, Union
from pydantic import BaseModel, BeforeValidator, Field, FilePath
def FirstOf(*paths: str | Path):
# Generator expression: must capture p with p=p
return Union[tuple(
Annotated[FilePath, BeforeValidator(lambda _, p=p: Path(p))]
for p in paths
)]
class MyModel(BaseModel):
my_path: FirstOf("a.txt", "b.txt") = Field(default=None, validate_default=True)
print(" >> Setup: create only a.txt")
Path("a.txt").touch()
Path("b.txt").unlink(missing_ok=True)
m1 = MyModel()
print(f"Selected: {m1.my_path}") # a.txt
print()
print(" >> Setup: create only b.txt")
Path("a.txt").unlink(missing_ok=True)
Path("b.txt").touch()
m2 = MyModel()
print(f"Selected: {m2.my_path}") # b.txt
from pathlib import Path
from typing import Annotated
from pydantic import BaseModel, BeforeValidator, Field, FilePath
def Candidate(p: str | Path):
# Safe without p=p; closure is unique per call
return Annotated[FilePath, BeforeValidator(lambda _: Path(p))]
class MyModel(BaseModel):
my_path: Candidate("a.txt") | Candidate("b.txt") = Field(
default=None, validate_default=True
)
print(" >> Setup: create only a.txt")
Path("a.txt").touch()
Path("b.txt").unlink(missing_ok=True)
m1 = MyModel()
print(f"Selected: {m1.my_path}") # a.txt
print()
print(" >> Setup: create only b.txt")
Path("a.txt").unlink(missing_ok=True)
Path("b.txt").touch()
m2 = MyModel()
print(f"Selected: {m2.my_path}") # b.txt
@lmmx
Copy link
Author

lmmx commented Feb 3, 2026

Alt. forms, including a rare __class_getitem__, which'd let you do a Literal-like square bracketed hint here

Screenshot from 2026-02-03 20-50-05

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment