Language Fundamentals
A .vy file reads like Python, but it means Daml. Every type, function, and
statement you write has a direct Daml equivalent, and the compiler emits exactly
that equivalent. If you know Python, the syntax is familiar. If you know Daml, the
behaviour is familiar. This page covers both sides.
The examples build one module step by step. Joined in order, the python blocks
on this page form one valid file, Fundamentals.vy, which compiles and builds
into a DAR. Blocks marked py show code the compiler rejects or that you
should avoid.
How .vy differs from Python
Section titled “How .vy differs from Python”Keep these rules in mind. Most of the rest follows from them.
- Nothing executes.
forgeparses the file and lowers it to Daml. Python’s runtime, standard library, and third-party packages aren’t available. Only the constructs on this page are. - Values are immutable. Daml has no mutation. When you reassign a variable or
a field, the compiler rewrites it as a new value: a
letbinding, a record update, or a fold. - Types are checked at compile time. Every function parameter and return value needs an annotation. Mismatches fail before Daml is generated.
- Contracts are never edited. A contract can only be created or archived. “Changing” a contract means archiving it and creating a new one with a new contract ID.
Imports
Section titled “Imports”from datetime import date, datetime, timedeltafrom enum import Enum, autofrom typing import Optional
from forge.builtins.daml_lang import create, exercise, fetchfrom forge.models import ContractId, Decimal, GenMap, Party, TextMap, choice, template, variantImport rules:
- Contract decorators (
@template,@variant,@choice,@view,@variant_record) and DamlForge types come fromforge.models. An unimported decorator fails withDF1219. - Dates and times use Python’s own
datetimemodule. - Enums use Python’s
enummodule. - Ledger operations (
create,fetch,exercise,archive,fetch_by_key) come fromforge.builtins.daml_lang. Import each one you use. len,range,ceil, andfloorare built in and need no import.- Aliased from-imports (
from X import Y as Z) and wildcard imports are rejected withDF1213. When two packages export the same name, use a module alias such asimport Splice.Api.FeaturedAppRightV1 as FeaturedAppRightV1.
Native types
Section titled “Native types”.vy type |
Daml type | Literal or constructor |
|---|---|---|
bool |
Bool |
True, False |
int |
Int |
42 |
Decimal |
Decimal |
Decimal(0), Decimal(1.5) |
str |
Text |
"hello" |
Party |
Party |
Comes from the ledger, never written as a literal |
ContractId[T] |
ContractId T |
Returned by create, passed in as an argument |
date |
Date |
date(2026, 12, 31) |
datetime |
Time |
datetime.now() inside an update |
timedelta |
RelTime |
timedelta(days=7, hours=12) |
Optional[T] |
Optional T |
A plain T value, or None |
list[T] |
[T] |
[1, 2, 3] |
tuple[A, B] |
(A, B) |
(1, "one") |
GenMap[K, V] |
Map K V |
{alice: Decimal(0)} |
TextMap[V] |
TextMap V |
{"key": "value"} |
bytes, complex, object, type, and other types with no Daml equivalent are
rejected with DF1222. dict[K, V] is rejected with DF1213; use GenMap or
TextMap instead.
Functions
Section titled “Functions”A top-level def becomes a Daml function with an explicit signature. Annotate
every parameter and the return type:
def increment(n: int) -> int: return n + 1
def greet(name: str) -> str: return "Hello, " + name
def half(amount: Decimal) -> Decimal: return amount / Decimal(2)increment : Int -> Intincrement n = n + 1
greet : Text -> Textgreet name = "Hello, " <> nameArithmetic, comparison, and boolean operators keep their Python spelling. A few of them lower to a different Daml operator:
.vy |
Daml |
|---|---|
a + b on str |
a <> b |
a + b on lists |
a ++ b |
a and b, a or b, not a |
a && b, a || b, not a |
a != b |
a /= b |
a // b |
A generated helper with Python’s floor-division semantics |
len(xs) |
length xs |
The compiler checks calls to your own functions. A wrong argument count fails with
DF1225, and an argument of the wrong type fails with DF1226.
A function can’t call itself. Direct recursion is rejected with DF1209. Use a
for loop, which compiles to a fold, instead.
Assembling types
Section titled “Assembling types”Tuples
Section titled “Tuples”Tuples group values without naming them. Index them with [0], [1], and so
on:
def coords() -> tuple[int, int]: return (1, 2)
def pair_sum(pair: tuple[int, int]) -> int: return pair[0] + pair[1]pair_sum : (Int, Int) -> Intpair_sum pair = (pair._1) + (pair._2)def append_one(xs: list[int]) -> list[int]: return xs + [1]
def first_or_zero(xs: list[int]) -> int: if len(xs) == 0: return 0 return xs[0]xs + [1] builds a new list and leaves xs unchanged. Indexing with xs[i]
lowers to DA.List.(!!), which walks the list. Daml lists are linked lists, so
iterate with for where you can.
Structs
Section titled “Structs”A struct is a named record, the Daml data T = T with .... Use it to give
structure to contract data:
struct Bank: party: Party name: str
struct Account: number: str bank: Bankdata Bank = Bank with party : Party name : Text deriving (Eq, Show)Build a struct with keyword arguments and read fields with dot access. Every
struct derives Eq and Show, so == works on it:
struct Point: x: int y: int
def shift(p: Point, dx: int) -> Point: return Point(x=p.x + dx, y=p.y)Structs hold data only. A def inside a struct body is dropped without any
error, so put helper logic in a top-level function:
struct Point: x: int y: int
def norm(self) -> int: return self.x + self.yAn enum is a sum type whose constructors carry no data. Use Python’s Enum with
bare names or auto(). Explicit values such as USD = 1 are rejected with
DF1231:
class Currency(Enum): USD = auto() EUR = auto()
def is_euro(c: Currency) -> bool: return c == Currency.EURdata Currency = USD | EUR deriving (Eq, Show)
is_euro : Currency -> Boolis_euro c = c == EURVariants
Section titled “Variants”When constructors carry data, use @variant. Each method declares one
constructor, and its parameters are the payload:
@variantclass Location: def InHand(): pass def InAccount(account: Account): passdata Location = InHand | InAccount Account deriving (Eq, Show)Constructors are positional by default, so parameter names don’t appear in Daml.
When the Daml API needs named fields (Constructor with field : T), mark the
constructor with @variant_record, imported from forge.models.
Variants covers constructing, comparing, and testing variant
values, and what isn’t supported yet.
Optional values
Section titled “Optional values”Optional[T] is Daml’s replacement for null. Write plain values: the compiler
inserts Some and fromSome based on the declared types.
def maybe_name(flag: bool, name: str) -> Optional[str]: if flag: return name return None
def name_or_default(name: Optional[str]) -> str: if name is not None: return name return "anonymous"maybe_name : Bool -> Text -> (Optional Text)maybe_name flag name = ... if flag then Some name ...
name_or_default : (Optional Text) -> Textname_or_default name = ... if name /= None then fromSome name ...Never write Some(...) or from_some(...) yourself. Check is not None before
using the value: unwrapping a None fails the transaction at runtime.
GenMap[K, V] maps any comparable key type to values. TextMap[V] uses str
keys. Both are immutable, so insert returns a new map:
def has_key(names: GenMap[Party, str], key: Party) -> bool: return names.member(key)
def get_label(names: GenMap[Party, str], key: Party) -> Optional[str]: return names.lookup(key)
def add_label(names: GenMap[Party, str], key: Party, label: str) -> GenMap[Party, str]: return names.insert(key, label)
def find_name(names: TextMap[str], key: str) -> Optional[str]: return names.lookup(key)Maps also support .delete(k), .keys(), and .values().
On template fields, you can use subscript and in syntax: self.names[who],
self.names[who] = label, and who in self.names. See
Changing a contract.
Known limitation: For maps passed as function parameters, use the method forms.
key in namesandnames[key]on a parameter currently generate list operations, anddamlcrejects the result.
Control flow
Section titled “Control flow”Branches
Section titled “Branches”if, elif, and else compile to nested Daml if ... then ... else. Every
path of a function must return a value of the declared type:
def describe(n: int) -> str: if n < 0: return "negative" elif n == 0: return "zero" else: return "positive"
def larger(a: int, b: int) -> int: return a if a > b else bA conditional expression (a if cond else b) maps one-to-one to Daml’s inline
if. An early return is fine: the compiler moves the code after the if into
the else branch.
Daml has no mutable loop variable. A for loop compiles to a foldl that
threads every reassigned variable through as an accumulator:
def total(values: list[Decimal]) -> Decimal: acc: Decimal = Decimal(0) for value: Decimal in values: acc = acc + value return acctotal : [Decimal] -> Decimaltotal values = let (acc : Decimal) = (0.0 : Decimal) in let _damlforge_acc2 = foldl (\acc value -> let _damlforge_acc1 = acc + value in _damlforge_acc1) acc values in _damlforge_acc2The loop-variable annotation (value: Decimal) is .vy syntax. It’s optional
when the iterable’s type is known. range(start, stop) lowers to
[start .. stop - 1], and if inside a loop body works as expected:
def evens(n: int) -> list[int]: result: list[int] = [] for i in range(0, n): if i % 2 == 0: result = result + [i] return resultwhile loops have no fold equivalent, so they aren’t supported.
These loops are pure. A loop that reads or writes the ledger compiles to
foldlA instead. See Loops with ledger effects.
Assertions
Section titled “Assertions”assert condition, "message" lowers to assertMsg. A failed assertion aborts
the whole transaction, so use it in choices to reject invalid input. An example
appears in Changing a contract.
Dates and time
Section titled “Dates and time”Use Python’s datetime types. The compiler maps each operation to DA.Time or
DA.Date:
def deadline(start: datetime) -> datetime: return start + timedelta(days=7)
def year_end() -> date: return date(2026, 12, 31)
def stamp() -> datetime: return datetime.now()deadline : Time -> Timedeadline start = addRelTime start (days 7)
year_end : Dateyear_end = date 2026 Dec 31
stamp : Update Timestamp = do getTime- Months are Python-style integers,
1through12. timedeltasupportsweeks,days,hours,minutes,seconds,milliseconds, andmicroseconds. Calendar years aren’t fixed-length, soyears=isn’t supported.- Subtracting two
datetimevalues gives atimedelta(subTime). datetime.now()reads ledger time, sostampreturnsUpdate Time. The next section explains why.- Only these constructs are translated, not the whole
datetimemodule. Attributes and methods such as.date()or.weekday()aren’t available yet; Working with Time lists what works and theDA.Datehelpers to use instead.
Templates and contracts
Section titled “Templates and contracts”A template is a contract schema. Think of it as a table where each contract is a row. Fields are the columns, and a few reserved names hold the contract’s authorization rules:
@templateclass CashBalance: owner: Party bank: Bank currency: Currency amount: Decimal location: Location
signatory: list[Party] = [bank.party, owner] ensure: bool = amount > Decimal(0)
@choice(controller=owner, consuming=True) def move(self, new_location: Location) -> ContractId[CashBalance]: return create(CashBalance( owner=self.owner, bank=self.bank, currency=self.currency, amount=self.amount, location=new_location, ))template CashBalance with owner : Party bank : Bank currency : Currency amount : Decimal location : Location where signatory [bank.party, owner] ensure amount > (0.0 : Decimal)
choice Move : ContractId CashBalance with new_location : Location controller owner do let self = this create (CashBalance with owner = self.owner, bank = self.bank, currency = self.currency, amount = self.amount, location = new_location)| Clause | Meaning | Rule |
|---|---|---|
signatory |
Parties whose authority the contract needs | Required (DF1201). A single Party or a list[Party] |
observer |
Parties who can see the contract | Optional |
ensure |
A precondition checked on every create | Must be a bool comparison, boolean operation, or call (DF1212) |
key / maintainer |
A contract key and the parties who maintain it | Declare both or neither (DF1210, DF1211) |
An __init__ method whose body contains assert statements also becomes
ensure. The examples in the repository use that form.
A choice is a method decorated with @choice. controller names the parties
who may exercise it. consuming=True archives the contract when the choice is
exercised, and consuming=False leaves it active (nonconsuming in Daml). Choice
names are capitalized in Daml: move becomes Move. Use @DamlName("Name"),
imported from forge.builtins, to choose the Daml name yourself.
controller and observer may only refer to template fields, the choice’s own
arguments, or self.<field>. Anything else fails with DF1224.
Changing a contract
Section titled “Changing a contract”Contracts are immutable, but a consuming choice can assign to self. The
compiler turns each assignment into a record update and creates a new contract
from the final value:
@templateclass Counter: owner: Party count: int history: list[int]
signatory: Party = owner
@choice(controller=owner, consuming=True) def bump(self, by: int) -> int: assert by > 0, "by must be positive" self.count = self.count + by self.history = self.history + [by] return self.count
@choice(controller=owner, consuming=False) def peek(self) -> int: return self.count
@templateclass Registry: admin: Party names: GenMap[Party, str]
signatory: Party = admin
@choice(controller=admin, consuming=True) def register(self, who: Party, label: str) -> int: if who in self.names: self.names[who] = self.names[who] + "!" else: self.names[who] = label return len(self.names.keys()) choice Bump : (ContractId Counter, Int) with by : Int controller owner do let self = this assertMsg "by must be positive" (by > 0) let _damlforge_self3 = self { count = self.count + by } let _damlforge_self4 = _damlforge_self3 { history = _damlforge_self3.history ++ [by] } _damlforge_new_self <- create _damlforge_self4 pure (_damlforge_new_self, _damlforge_self4.count)The declared return type was int, but the Daml choice returns
(ContractId Counter, Int). The new contract’s ID comes first, followed by your
return value. Callers need the new ID because the old one is now archived. Any
reference to the old ID is stale: fetching it fails.
Don’t also call create for the same template in a choice that mutates self.
That creates a second copy of the contract.
Ledger effects
Section titled “Ledger effects”Reading or writing the ledger is an update in Daml, and a function that does
this must return Update T. DamlForge infers this for you. Any function that
calls create, fetch, exercise, archive, a by-key lookup, or datetime.now(),
directly or through another function that does, is emitted as an update:
def read_count(cid: ContractId[Counter]) -> int: counter = fetch(cid) return counter.count
def bump_once(cid: ContractId[Counter]) -> int: result = exercise(cid, cid.bump(by=1)) return result[1]read_count : (ContractId Counter) -> Update Intread_count cid = do counter <- fetch cid pure (counter.count)
bump_once : (ContractId Counter) -> Update Intbump_once cid = do result <- exercise cid (Bump with by = 1) pure (result._2)exercise(cid, cid.choice_name(arg=value)) exercises a choice on a contract ID.
Choice arguments are always passed by keyword. Here, bump mutates self, so
the result is a tuple, and result[1] is the declared int.
Choice bodies are already updates, so they can call ledger operations directly.
In every case, import the operations from forge.builtins.daml_lang, as at the
top of this page.
Loops with ledger effects
Section titled “Loops with ledger effects”A for loop whose body calls fetch, create, exercise, or any other update
can’t be a pure foldl, because each step has to run on the ledger. The compiler
emits foldlA from DA.Action instead, and imports it for you. Each step is a
do block that runs the effect, then passes the accumulator on:
def total_count(cids: list[ContractId[Counter]]) -> int: total: int = 0 for cid in cids: counter = fetch(cid) total = total + counter.count return totalimport DA.Action (foldlA)
total_count : [ContractId Counter] -> Update Inttotal_count cids = do let (total : Int) = 0 _damlforge_total2 <- foldlA (\total cid -> do counter <- fetch cid let _damlforge_total1 = total + counter.count pure _damlforge_total1) total cids pure (_damlforge_total2)The _damlforge_ names are compiler-generated, and their numbers depend on what
else is in the module.
The effect can also sit inside an expression or an if test. The compiler lifts
it into its own <- step first:
def count_above(cids: list[ContractId[Counter]], limit: int) -> int: n: int = 0 for cid in cids: if fetch(cid).count > limit: n = n + 1 return n foldlA (\n cid -> do _damlforge_effect1 <- fetch cid if _damlforge_effect1.count > limit then do let _damlforge_n2 = n + 1 pure _damlforge_n2 else do pure n) n cidsTo collect results, append to a list. Here, each step creates a contract and
adds its ID:
def open_counters(owners: list[Party]) -> list[ContractId[Counter]]: cids: list[ContractId[Counter]] = [] for owner in owners: cids.append(create(Counter(owner=owner, count=0, history=[]))) return cids foldlA (\cids owner -> do _damlforge_cids_item1 <- create (Counter with owner = owner, count = 0, history = []) let _damlforge_cids2 = cids <> [_damlforge_cids_item1] pure _damlforge_cids2) cids ownersEffectful loops work the same way in choice bodies and in free functions. A free function containing one is inferred as an update, like any other function that touches the ledger.
The same limits apply as for pure loops. There’s no break or continue, and a
loop can’t both update a variable and return from inside its body (DF1230).
Modules
Section titled “Modules”Each .vy file is one Daml module, named after the file: Fundamentals.vy
becomes module Fundamentals. Within a file, declarations can refer to each
other in any order. To use a declaration from another .vy file in the same
project, import it by module name:
from Fundamentals import CashBalance, CurrencyTypes from packages written in plain Daml are imported the same way, using the
Daml module name, for example from Splice.AmuletRules import AppTransferContext.
The package must be listed under data-dependencies in daml.yaml. Run
forge stubs on its DAR if you want readable .vy declarations for it.
Next steps
Section titled “Next steps”You now have the building blocks: types, functions, control flow, and the
template model. When the compiler rejects something, look up its DF12xx code
in Compiler Errors. The next module,
Contract Templates, builds your first contracts and
tests them with Daml Script.