Skip to content

modifiers and visibility

basedpython promotes commonly-used decorators and typing annotations into first-class keyword modifiers on classes, functions, and assignments. the surface keywords replace boilerplate decorator/annotation pairs at transpile time:

data class Point:
    x: int
    y: int

    override def __repr__(self) -> str:
        return f"({self.x}, {self.y})"

let ORIGIN = Point(0, 0)

transpiles to:

from dataclasses import dataclass
from typing import Final
from typing_extensions import override

@dataclass(slots=True)
class Point:
    x: int
    y: int

    @override
    def __repr__(self) -> str:
        return f"({self.x}, {self.y})"

ORIGIN: Final = Point(0, 0)

class modifiers

basedpython Python output
final class Foo @final + class Foo
abstract class Foo class Foo (keyword stripped, no decorator)
open class Foo class Foo (keyword stripped, no decorator)
data class Foo @dataclass(slots=True) + class Foo
frozen data class Foo @dataclass(frozen=True, slots=True) + class Foo
protocol Foo class Foo(Protocol) (base added)
sealed class Foo class Foo + Foo.__sealed_members__ = (...)

abstract is a marker for the type checker; it has no runtime decorator. open is the inverse of final — a marker that the class is intended to be subclassed. neither emits a runtime artefact

sealed declares a closed subclass hierarchy — see sealed classes

frozen data class rejects every attribute write after construction, so its fields are read-only and the class is inferred covariant in their types:

frozen data class D[T]:
    t: T

d: D[object] = D[int](t=1)   # ok — `D` is covariant in `T`

a plain data class is mutable, so it stays invariant. the same rule applies to any frozen dataclass-like class: @dataclass(frozen=True), a @dataclass_transform(frozen_default=True) base, a frozen pydantic model, and individual pydantic fields marked Field(frozen=True)

enum class is not a modifier but its own declaration form — see based enums

function modifiers

basedpython Python output
final def m @final def m
abstract def m @abstractmethod def m
override def m @override def m
static def m @staticmethod def m
class def m @classmethod def m

override is sourced from typing on 3.12+ and typing_extensions below. abstract def with no body is filled in with : raise NotImplementedError instead of the usual : ...

let / var / class-var / newtype

basedpython Python output
let MAX = 100 MAX: Final = 100
let x: int x: Final[int]
let x x: Final
var x = 1 x = 1
var x: int = 1 x: int = 1
var x: int x: int
class count = 0 count: ClassVar = 0 (inside a class)
newtype UserId = int UserId = NewType("UserId", int)

let works at module and class scope. inside a class, class x = ... is the class-variable form (distinct from the regular let x = ... which is Final). the initializer may be omitted: let x: int declares a read-only attribute and a bare let x an uninitialized Final, both bound by a single later assignment. newtype introduces a distinct typing.NewType-backed type at module scope

var

var is the mutable counterpart of let: it marks the declaration site of a variable and nothing else. the keyword is stripped at transpile time and the statement means exactly what the assignment under it means — no Final, and an untyped var puts no declared type on the name:

var count = 0
count = 1        # fine; `let count = 0` would reject this

var name: str = ""
name = 1         # error: `str` is declared

var works at module, class, and function scope, and composes with the modifier keywords (private var x = 1). unlike let, it may not be written bare: var x states neither a type nor a value, so there is nothing to declare and it is rejected — write var x: T or let x

var on an init(...) parameter is a different feature — the attribute shorthand described in init method

assignment modifiers

override, final override, and abstract may also appear on assignments and annotated assignments. the modifier keyword is stripped at transpile time:

basedpython Python output
override x = 1 x = 1
final override x = 1 x = 1
abstract x: T x: T

these are compile-time-only markers — they constrain how the symbol is checked but emit no runtime artefact

a bare final x = 1 (with no override) is not an assignment modifier: it would strip to a plain x = 1 and declare nothing final. outside a class body ty rejects it with final-on-variable and points you to let, which lowers to Final. inside a class body it is a plain attribute, matching let there, and is not flagged

export / public / private

basedpython infers __all__ from explicit visibility keywords:

export def public_api(): ...
public def also_exported(): ...
private def helper(): ...

transpiles to:

__all__ = ["public_api", "also_exported"]

def public_api(): ...
def also_exported(): ...
def _helper(): ...
  • export and public are aliases. each marked symbol is added to a synthesized __all__ list at module level
  • private strips the keyword and renames the symbol with a leading underscore at the definition site and every same-module call site. it is excluded from __all__ even when no export/public declarations exist
  • inside a class body only private means anything — export/public are stripped. what private renames depends on the member: a private def is name-mangled (__helper), a private property becomes _x with __x storage, and a private attribute keeps its name. either way the member is private to the type checker, which is what safe variance rests on

inlay hints

a method that overrides a superclass member without saying so gets an override inlay hint, written where the modifier would go. the hint navigates to the superclass it overrides:

class B(A):
    ⟨override ⟩def f(self): ...

constructor-like methods (__init__, __new__, __post_init__, __init_subclass__) and name-mangled private methods are exempt, matching missing-override-decorator