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:
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 : ...
override, static and class are method modifiers: they say how a class
dispatches one of its members, or that the member replaces one it inherits. a
def that no class body owns is not a member of anything, so writing one on it
is an error:
final, abstract and the visibility keywords read on a function wherever it
is written
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) |
class var count: int |
count: ClassVar[int] |
class let ORIGIN: P = P() |
ORIGIN: Final[P] = P() |
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
a let or var written inside a block — an if body, a loop body, a try clause —
belongs to that block and is gone after it. see block scoping
class var x: T is the same class variable with its type declared rather than
read off a value, which is the only form a stub can write. class let x: T = v
is the read-only one — python spells that Final, which in a class body cannot
be reassigned through the class or an instance. a class let needs a value:
__init__ binds an instance, so there is no later place for one to arrive.
class is a class-body modifier — at module scope there is no class for the
variable to belong to, and it is an error there
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:
transpiles to:
__all__ = ["public_api", "also_exported"]
def public_api(): ...
def also_exported(): ...
def _helper(): ...
exportandpublicare aliases. each marked symbol is added to a synthesized__all__list at module levelprivatestrips the keyword and gives the symbol a leading underscore at the definition site and every same-module call site. a name that already has one keeps it — a second would make it a__name, which python name-mangles wherever a class body reads it. it is excluded from__all__even when noexport/publicdeclarations exist- inside a class body only
privatemeans anything —export/publicare stripped. whatprivaterenames depends on the member: aprivate defis name-mangled (__helper), aprivateproperty becomes_xwith__xstorage, and aprivateattribute keeps its name. either way the member is private to the type checker, which is what safe variance rests on - a call to a
private defis written with the mangled name spelled out —self.helper()becomesself._A__helper(). python mangles lexically, so a bareself.__helperwould name a different attribute in a subclass's body and none at all outside a class; the full spelling reaches the method from all of them privateon a name python looks up verbatim — a dunder, or_— is reported as having no effect. mangling applies only to a name with at most one trailing underscore, so renaming would change what the member is rather than who can reach it, and leaving it alone would make the modifier do nothing. the one dunder whereprivatesays something isinit, which is checked at the construction site instead
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:
constructor-like methods (__init__, __new__, __post_init__,
__init_subclass__) and name-mangled private methods are exempt, matching
missing-override-decorator