Skip to content

differences from python

.by is not a superset of .py. almost all python means the same thing in basedpython, and this page is the rest of it: every construct that reads differently, so that renaming a .py file to .by is a decision rather than a formality

each one is a deliberate fix to something python cannot change without breaking the world

you don't have to port by hand

by transpile --reverse rewrites python source into basedpython idioms, including the constructs below

runtime behaviour

the same source, running, does something else

is is an instance check

is means isinstance and === means identity:

you write python does
x is y isinstance(x, y)
x is not y not isinstance(x, y)
x === y x is y
x !== y x is not y

the compiler doesn't always do isinstance, for example x is None will become x is None in python, this is because "type of x is None" and "value of x is None" have identical meanings

see identity and isinstance

a mutable default is re-evaluated per call

def append_one(items=[]):
    items.append(1)
    return items

python returns an ever-growing list; basedpython returns [1] every time. only non-scalar defaults are affected — numbers, bools, strings, None and ... stay as plain python defaults. see default argument re-evaluation

a loop target is a fresh binding per iteration

fns = []
for i in [1, 2, 3]:
    fns.append(lambda: print(i))

python prints 3 3 3, because the loop has one cell shared by every iteration. basedpython prints 1 2 3. comprehension targets bind the same way. see unique loop bindings

imports are lazy by default

every import and from ... import in a .by file is marked lazy, so the module's body does not execute until something first touches it:

import os

print(os)   # this is what loads it

an import with a side effect — registering a plugin, patching something at module scope — no longer happens just because the importing module was loaded. from __future__ import ..., from x import *, and an unaliased import a.b stay eager. see lazy imports

what an annotation means

the same annotation denotes a different type

a string is a string type, not a forward reference

x: "Foo"

python reads that as a deferred reference to the name Foo; basedpython reads it as the string-literal type. there is no manual forward-reference syntax because none is needed — the transpiler quotes a self-reference for you when the runtime requires it

float means float

python's typing spec special-cases float to mean int | float, and complex to mean int | float | complex. basedpython does not:

def takes(x: float) -> None: ...

takes(1)   # rejected

see strict float and complex

[T: (int, str)] is a tuple bound, not constraints

python reads a tuple after a type parameter's : as a list of discrete constraints, which leaves no way to say "bounded by the type tuple[int, str]". basedpython reads the : literally — it is a bound like any other:

def bounded[T: (int, str)](x: T) -> T:
    reveal_type(x[0])  # int

the constraint set is reframed as a "type mapping", and gets its own keyword, in:

def constrained[T in (int, str)](x: T) -> T: ...

so [T: (int, str)] ported from python means something else, and wants rewriting to [T in (int, str)] — which is what by transpile --reverse does for you. see type mappings

class A[**Kwargs] is keyword type arguments, not a parameter specification

python's typing denotes that **P is a parameter specification, but basedpython generalises the concept as an upper bound of a standard type parameter:

def f[P: (*: *, **: *)](fn: (**P) -> None): ...

f[(int, str, foo: bool)]

class HasKeywords[**Kwargs]

HasKeywords[foo=int, bar=str]

see generics

a parameter specification is forwarded with stars

python names a parameter specification's two halves as attributes of the type variable, *args: P.args and **kwargs: P.kwargs. basedpython unpacks them the way it unpacks every other pack, and the attribute spelling is an error:

def deco[P: (*: *, **: *), R](fn: (**P) -> R) -> (**P) -> R:
    def inner(*args: *P, **kwargs: **P) -> R:
        return fn(*args, **kwargs)

    return inner

see forwarding

type checking

the code runs the same; the checker's verdict differs

an unsolved type variable is Never

where a type variable is never solved, python's checkers infer Unknown and stop checking. basedpython infers Never in covariant and bivariant positions, which keeps checking. see precise unsolved type variables

inference is sound where python's is gradual

basedpython infers a precise type in places the spec allows a gradual one, so code that leaned on Any flowing through silently now reports. see sound types

the stdlib is typed differently

typeshed improvements lists the whole set. the ones most likely to report on existing code:

  • an optional re capture group is str | None, not Any, so m.group(1).upper() is an error
  • a functools.cached function keeps its parameter list, so a wrong-arity call to it is an error
  • dict / set keys are bounded by Hashable
  • a membership test checks that the operands overlap, so "a" in [1, 2] is an error rather than a guaranteed False