Type Annotations & Controller Overrides
Type Frappe controllers with DF field annotations, cast, TypedDict, and TYPE_CHECKING imports.
On this page
Enabling Type Annotations
In hooks.py:
export_python_type_annotations = TrueThis enables the DF.* type annotation system from frappe.types, allowing field-level type declarations on DocType controllers.
Field Type Annotations
Import DF from frappe.types and annotate fields on the controller class:
from frappe.types import DF
class MyDocType(Document):
# Standard field types
customer_name: DF.Data
amount: DF.Currency
is_active: DF.Check
description: DF.TextEditor
# Optional fields (custom or nullable)
part_group: DF.Link | None
pro_number: DF.Int | None
surface_area: DF.Float | None
charge: DF.Currency | None
markup: DF.Percent | None
# Child table fields
items: DF.Table[BOMItem]
operations: DF.Table[BOMOperation]
# Table MultiSelect
guardians: DF.TableMultiSelect[FamilyGuardian]
# Select with literals
status: DF.Literal["Open", "Completed", "Cancelled"]Common DF.* Types
| Annotation | Frappe Fieldtype |
|---|---|
DF.Data | Data |
DF.Int | Int |
DF.Float | Float |
DF.Currency | Currency |
DF.Percent | Percent |
DF.Check | Check |
DF.Link | Link |
DF.Table[ChildType] | Table |
DF.TableMultiSelect[ChildType] | Table MultiSelect |
DF.TextEditor | Text Editor |
DF.Literal[...] | Select |
Controller Class Override Pattern
Simple Override (Custom Fields Only)
When you only need type annotations for custom fields:
from erpnext.stock.doctype.item.item import Item as BaseItem
from frappe.types import DF
class Item(BaseItem):
"""Custom Item with additional fields."""
valuation_uom: DF.Link | NoneComplex Override (Methods + Typed Children)
from typing import Any, cast
import frappe
from erpnext.manufacturing.doctype.bom.bom import BOM as BaseBOM
from frappe.types import DF
class BOM(BaseBOM):
"""Custom BOM override for app-specific functionality."""
# Inline imports for child table types (avoids circular imports)
from myapp.mymodule.doctype.bom_part_group.bom_part_group import BOMPartGroup
part_group_measurements: DF.Table[BOMPartGroup]
cadwork_guid: DF.Data
# Override standard child table types for stronger typing
from myapp.myapp.custom.bom_item import BOMItem
items: DF.Table[BOMItem]
# Transient attributes (not persisted to DB)
_import_warnings: dict[str, list[dict[str, Any]]] | None = None
def validate(self) -> None:
super().validate()
# Custom validation logic
@frappe.whitelist()
def custom_action(self) -> str:
# Whitelisted method callable from client
...Key Conventions
Alias the base class to avoid name collision:
from erpnext.stock.doctype.item.item import Item as BaseItemCustom fields use
| Nonesince they may not exist in the base schema:my_custom_field: DF.Float | NoneInline imports for child table types prevent circular import issues:
class BOM(BaseBOM): from myapp.custom.bom_item import BOMItem items: DF.Table[BOMItem]Transient attributes (not saved to DB) use class-level defaults:
_cache: dict[str, Any] | None = None
Using cast for Type Safety
When frappe.get_doc() returns a generic Document, use cast to get type checking:
from typing import cast
doc = cast("Item", frappe.get_doc("Item", item_code))
# Now doc.valuation_uom, doc.uoms, etc. are typed
# Also useful for whitelisted endpoint return values
doc = cast("Item", frappe.get_doc("Item", item_code))
for row in doc.uoms: # row is typed as UOMConversionDetail
if row.uom == sales_uom:
row.conversion_factor = flt(value)TypedDict for API Contracts
Use TypedDict to type filter parameters, query results, and API responses:
from typing import TypedDict
from frappe.types import DF
class Filters(TypedDict, total=False):
project: DF.Link | None
sales_quote: DF.Link | None
date_range: tuple[str, str]
status: DF.Literal["Open", "Completed", "Cancelled"]
class QueryResult(TypedDict, total=False):
id: str
name: str
quoted_hours: float
actual_hours: float
indent: int
def execute(filters: Filters | None = None) -> tuple[list[Any], list[QueryResult], None, Chart]:
...Conditional Imports with TYPE_CHECKING
For imports only needed by type checkers (not at runtime):
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from myapp.myapp.custom.item import Item
def update_item(item_code: str) -> dict:
doc = cast("Item", frappe.get_doc("Item", item_code))
...Auto-Generated Type Blocks
Frappe can auto-generate type annotations for DocType controllers. These go in a guarded block:
class Family(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from my_app.my_module.doctype.family_guardian.family_guardian import FamilyGuardian
address: DF.Link | None
guardians: DF.TableMultiSelect[FamilyGuardian]
# end: auto-generated typesCustom logic and additional annotations go outside this block.
This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.