
[July 2026 edition]
Deep Insights into Nim Syntax — How Similar Is It to Pascal, Python, and Haskell?
Published: Jul 24, 2026
Reading time: ~30 min
Scope of This Article
The main subject of this article is a full comparison of how each Nim construct resembles the styles of Pascal, Python, C-family, and functional languages (Part 5, 38 topics in total). It covers not only block syntax but also declarations, the type system, expression orientation, generics, metaprogramming, and compile-time execution. We do not discuss historical adoption — which language a feature was taken from. What matters to the person writing code is what style of syntax it is.
As a prerequisite, we first organize the designs of the languages used for comparison.
- A syntax comparison of Pascal and Python (Part 1)
- The difference between “Pascal’s indentation is formatting; Python’s indentation is syntax” (Part 2)
- An overview of languages where indentation forms blocks (off-side rule languages) (Part 3)
- Haskell’s layout rule as syntactic sugar (Part 4)
- A style comparison of Nim syntax (Part 5)
In the Nim part, we distinguish “looks similar” from “is semantically similar,” organizing each construct with concrete code examples. Statements about Nim are based on the official manual and tutorials as of Nim 2.2.10.
Part 1: Comparing Pascal and Python Syntax
Pascal and Python have both been widely used as “educational” languages, but their design philosophies differ. Pascal emphasizes describing algorithms rigorously, while Python emphasizes readability and productivity.
The main syntactic correspondences are:
| Item | Pascal | Python |
|---|---|---|
| Comments | {...} or (*...*) | # |
| Variable declaration | var x: Integer; | x = 0 |
| Constants | const PI = 3.14; | PI = 3.14 (convention) |
| Assignment | x := 10; | x = 10 |
| Comparison | = | == |
| Blocks | begin ... end | indentation |
| Statement terminator | ; | none |
Conditionals
if x > 10 then
begin
WriteLn('big');
end
else
begin
WriteLn('small');
end;if x > 10:
print("big")
else:
print("small")Loops (for)
for i := 1 to 10 do
begin
WriteLn(i);
end;for i in range(1, 11):
print(i)Loops (while)
while x < 10 do
begin
x := x + 1;
end;while x < 10:
x += 1Functions
function Add(a, b: Integer): Integer;
begin
Add := a + b;
end;def add(a, b):
return a + bIn Pascal, you return a value by assigning to the function name (Delphi also allows Result := ...). This style will be contrasted with Nim’s result variable later.
Arrays
var
a: array[1..5] of Integer;a[1] := 10;a = [0] * 5
a[0] = 10Pascal can define 1-based arrays; Python is always 0-based.
Records (structs)
type
Person = record
Name: String;
Age: Integer;
end;from dataclasses import dataclass
@dataclass
class Person:
name: str
age: intClasses
type
TPerson = class
public
procedure Hello;
end;class Person:
def hello(self):
print("Hello")Major syntactic differences
| Pascal | Python |
|---|---|
Blocks via begin/end | Blocks via indentation |
Assignment with := | Assignment with = |
= is comparison | == is comparison |
| Static typing | Dynamic typing (type hints optional) |
| Declarations required | No declarations |
| Compiled language | Primarily interpreted |
Design philosophy
Pascal emphasizes “making program structure explicit”: you declare variables and types first, then write the algorithm. Python focuses on “the person reading the code,” omitting declarations as much as possible and making structure visible through indentation alone.
Although Python’s indentation syntax looks different, the idea of “making blocks explicit” is shared with Pascal’s begin ... end. Nim combines Python’s indentation syntax with Pascal-like static typing and compilation — a language positioned between the two. We examine this in Part 5.
Part 2: Is Indentation “Formatting” or “Syntax”?
In the comparison table in Part 1, we wrote “blocks: begin...end vs. indentation” in a single line, but this is not merely a notational difference.
Pascal’s indentation is ignored by the compiler
In standard Pascal, you normally put a line break after begin and place end on its own line.
begin
x := 1;
y := 2;
end;However, indentation is syntactically unnecessary. The compiler does not look at whitespace or indentation. The following code is also syntactically correct.
begin
x := 1;
y := 2;
end;You can also omit the line break after begin.
begin x := 1; y := 2; end;This is valid Pascal too. That said, nearly all Pascal/Delphi/FPC coding conventions recommend the line-break style, because it makes the pairing of begin and end easier to see and nested blocks easier to follow.
The same applies to conditionals.
if x > 0 then
begin
print(x);
end;This compiles without indentation. In Pascal, indentation is for humans.
Python’s indentation is the syntax itself
In Python, by contrast, indentation is the syntax.
if x > 0:
print(x)Without indentation, you get an error.
Describing this difference as a “difference in formatting conventions” is inaccurate. What differs is the representation of the syntax tree (AST) itself.
In Pascal, blocks are represented by the tokens begin ... end.
begin
if A then
begin
...
end;
end;Indentation is purely visual; you could flush everything to the left margin without changing the syntax.
In Python, blocks are the indentation itself — INDENT/DEDENT.
if a:
if b:
...Here the following relationships hold:
- One level deeper = entering a block
- One level back = leaving a block
In other words:
- Pascal: the
begin/endpair creates the block - Python: the depth of indentation itself creates the block (indentation depth = nesting depth)
Python’s indentation is not mere layout: at the lexing stage it is converted into INDENT and DEDENT tokens. For Python, indentation is a “syntactic symbol,” just like parentheses or begin/end.
In that sense, indentation in Python plays a role closer to Lisp’s parentheses or C’s {} than to anything in Pascal. This is not a “visual difference” — it is a fundamental difference in language design: what represents a block.
Part 3: Languages Where Indentation Forms Blocks — the Off-side Rule
Besides Python, other languages exist where indentation itself forms blocks, though few are as widespread.
Haskell
main = do
print "Hello"
foo x =
if x > 0
then x
else -xHaskell adopts a layout rule. Indentation forms blocks, but you can also make them explicit with {} and ; when needed (detailed in Part 4).
Nim
if x > 0:
echo "positive"
else:
echo "negative"Nim also expresses blocks with indentation, like Python. Its syntax, however, closely resembles Pascal and Ada as well.
F#
if x > 0 then
printfn "positive"
else
printfn "negative"F# also adopts the off-side rule.
Elm
if x > 0 then
"positive"
else
"negative"A functional language where indentation becomes structure.
Idris / Agda
Proof assistants and dependently typed languages also adopt indentation-based syntax.
CoffeeScript
square = (x) ->
x * xA language compiled to JavaScript, with Python-like indentation syntax.
Cobra
A .NET language with Python-like syntax.
The shared idea
These languages adopt the “off-side rule” (or “layout rule”). The mechanism:
- The lexer tracks the column of each line
- Increased indentation opens a block
- Decreased indentation closes a block
Python’s INDENT/DEDENT follows the same idea.
Nim, noteworthy in relation to Pascal
Among these, Nim stands out. Nim has features closely resembling Pascal:
- Static typing
- Pascal-style keywords such as
var,type, andproc - A compiled language
while its block representation alone is Python-like indentation rather than begin/end. It is sometimes described as “a language fusing Pascal’s structure with Python’s layout.”
Part 4: Haskell’s Layout Rule — Indentation as Syntactic Sugar
In Haskell, if you write {} and ; explicitly, indentation is ignored.
Haskell’s layout rule (indentation) and explicit {}/; are two notations for the same syntax. The following two are equivalent.
Layout-rule version:
let
x = 1
y = 2
in x + yExplicit version:
let {
x = 1;
y = 2
} in x + yOnce you write {}, indentation carries no syntactic meaning inside that block.
let {
x = 1;
y = 2
} in x + yEven with scattered indentation like this, the syntax is fine (readability is another matter).
Decided independently per block
The switch is decided per block. You can mix: layout rule outside, explicit braces inside.
main = do
putStrLn "A"
let {
x = 1;
y = 2
}
print (x + y)Here:
- The
doblock is managed by indentation - The
letblock is managed by{}and;
Each is independent.
The difference from Python
Python has no block representation other than indentation. In Haskell, {} and ; are the underlying representation, and the layout rule is syntactic sugar that supplies them automatically.
Haskell’s compiler interprets the layout rule during lexing, internally inserting {, }, and ; before parsing. This design allows switching to the explicit notation whenever necessary.
Both are “indentation-based,” yet the architectures differ: for Python, indentation is the only syntax; for Haskell, indentation is an abbreviation of the explicit notation.
Part 5: Analyzing Nim Syntax — Which Language’s Style Does It Resemble?
We organize Nim’s syntax construct by construct, based on the official manual and design documents, asking how it resembles — and differs from — Python, Pascal, C-family, and functional languages.
The conclusion first
Nim’s syntax is not simply “Python’s syntax + Pascal’s type system.” More precisely, it is the following combination:
- Surface appearance and block structure: Python-like
- Declaration syntax, procedural vocabulary, type annotation order: Pascal/Modula-like
- Static typing, value types, pointers, FFI, low-level control: Pascal/C/C++-like
- Expression orientation, generics, pattern-like type constraints: functional/ML-like
- UFCS, templates, AST macros, compile-time execution: strongly Nim’s own
- Name resolution and notational flexibility: reducible to no single existing language
Nim is best understood as a language in which you write Pascal-style declarative, statically typed programs using Python-style layout, with C-level control and Lisp-level syntactic extensibility.
Below, we compare syntax and semantics separately — not just appearances.
1. Block syntax is Python-style, but the implementation differs
if temperature > 30:
echo "hot"
startFan()
else:
echo "normal"if temperature > 30:
print("hot")
start_fan()
else:
print("normal")if temperature > 30 then
begin
WriteLn('hot');
StartFan;
end
else
begin
WriteLn('normal');
end;Like Python, Nim starts a block by deepening indentation after a colon; the block ends when indentation returns to the previous depth.
The internal implementation is not identical to Python’s, however. Nim’s lexer annotates each token with the number of preceding spaces, and the parser manages a stack of indentation depths. The official manual explains that indentation itself is not made into a standalone token. The goal is the same as Python’s INDENT/DEDENT tokens, but the processing differs.
Importantly, indentation in Nim is not mere formatting either.
if enabled:
initialize()
start()
shutdown()This has the structure:
if
└─ initialize
└─ start
shutdownWhereas in the following, shutdown() is inside the condition:
if enabled:
initialize()
start()
shutdown()Compared with Python, Nim adds a restriction: tabs are not allowed in leading indentation — spaces only. The official grammar explicitly forbids tabs.
if enabled:
<TAB>start() # compile error: tabs not allowedAlso, there is no general alternative syntax for switching blocks to {} and ; as in Haskell.
# No Haskell-style alternative like this exists
if enabled: { start(); log(); }A single statement may share the line, though.
if enabled: start()This is not an alternative block notation; it works because the body after the colon is a single simple statement.
2. Variable declarations are Pascal-style; type inference is modern
var count: int = 10
let name: string = "Alopex"
const maxRetries = 3var
count: Integer;
name: String;
const
MaxRetries = 3;count = 10
name = "Alopex"
MAX_RETRIES = 3Nim’s declarations are Pascal-style, with the order name : type.
var count: intC-family is the reverse.
int count;Nim also distinguishes var, let, and const.
var mutableValue = 1
let immutableValue = 2
const compileTimeValue = 3| Declaration | Meaning |
|---|---|
var | Reassignable variable |
let | Value not reassignable after initialization |
const | Constant evaluated at compile time |
Python has no language-level let or const. Pascal has var and const, but Nim’s let resembles modern static immutable bindings as in ML, Rust, and Swift.
Nim also has type inference; the type is determined from the initial value.
var count = 10 # int
var ratio = 0.5 # float
var active = true # boolThis differs from Python’s dynamic typing.
var value = 10
value = "ten" # compile errorThe type of value is fixed to int at the first assignment and never changes. The official tutorial describes this as local type inference. In Python, the equivalent code is valid:
value = 10
value = "ten"What Nim shares with Python is the conciseness of omitting declarations — not dynamic typing.
3. Assignment is Python/C-style; unlike Pascal’s :=
x = 10x = 10x := 10;Nim uses = for assignment and == for comparison.
if x == 10:
echo "ten"This is Python/C/Java-style. In Pascal:
x := 10;
if x = 10 then
WriteLn('ten');Variable declarations are Pascal-like, but the assignment operator is not.
4. Procedures use Pascal vocabulary; return values are expression-oriented
proc add(a, b: int): int =
a + bfunction Add(a, b: Integer): Integer;
begin
Add := a + b;
end;def add(a: int, b: int) -> int:
return a + bNim calls ordinary functions proc, not function or def — vocabulary reminiscent of Pascal/Modula procedures.
The treatment of return values is more modern than Pascal’s: the trailing expression becomes the return value. You can also assign to result explicitly.
proc add(a, b: int): int =
result = a + bEarly returns are possible.
proc classify(value: int): string =
if value < 0:
return "negative"
if value == 0:
return "zero"
result = "positive"This is not Pascal’s traditional “assign to the function name” style (Add := a + b;). Nim’s result is an implicit return variable. The procedure declaration looks Pascal-like, while treating the trailing expression as the value is expression-oriented, like Rust, Scala, and ML-family languages.
5. Grouping same-typed parameters closely resembles Pascal
proc distance(x1, y1, x2, y2: float): float =
discardAll four parameters are float. Pascal has the same form.
function Distance(x1, y1, x2, y2: Real): Real;In Python you usually annotate each parameter individually.
def distance(
x1: float,
y1: float,
x2: float,
y2: float
) -> float:
...Nim can also separate different types with semicolons.
proc connect(host: string; port: int; secure: bool) =
discardCommas work too.
proc connect(host: string, port: int, secure: bool) =
discardUsing semicolons to delimit type groups closely resembles Pascal’s procedure declarations.
6. if looks Python-like but also works as an expression
let label =
if score >= 80:
"excellent"
elif score >= 60:
"pass"
else:
"fail"Python’s ordinary if statement returns no value.
if score >= 80:
label = "excellent"
elif score >= 60:
label = "pass"
else:
label = "fail"To get an expression in Python, you use the conditional expression.
label = "excellent" if score >= 80 else "pass"In Nim, a full block-form if can be an expression.
proc absolute(x: int): int =
if x >= 0:
x
else:
-xThis resembles Rust, Scala, Kotlin, and ML-family languages more than Python. Nim can be written as a procedural language, but many constructs can produce values — it sits between “statement-centric Pascal” and “expression-centric functional languages.”
7. case looks Pascal-style but is stronger
case status
of 200:
echo "OK"
of 400, 404:
echo "client error"
of 500..599:
echo "server error"
else:
echo "unknown"case Status of
200:
WriteLn('OK');
400, 404:
WriteLn('client error');
500..599:
WriteLn('server error');
else
WriteLn('unknown');
end;Nim’s case, of, and the range symbol .. bear a very strong resemblance to Pascal.
Python 3.10+ match is structural pattern matching.
match status:
case 200:
print("OK")
case 400 | 404:
print("client error")Nim’s basic case is not ML-style pattern matching over algebraic data types, but it statically branches on enums, integers, strings, and so on, with exhaustiveness checking available.
Moreover, case can be an expression in Nim.
let message =
case status
of 200: "OK"
of 404: "Not Found"
else: "Unknown"Here Nim is more expression-oriented than traditional Pascal.
8. Enums, subranges, and sets strongly resemble Pascal
Enums:
type
Status = enum
pending
running
completed
failedSubrange types:
type
Percentage = range[0..100]
var progress: Percentage = 80Set types:
type
Permission = enum
read
write
execute
let adminPermissions = {read, write, execute}
if write in adminPermissions:
echo "writable"Pascal also has enums, subrange types, and set types.
type
TPercentage = 0..100;
TPermission = (ReadPerm, WritePerm, ExecutePerm);
TPermissions = set of TPermission;These are not core type features of Python; Python combines enum.Enum, set, and validation separately. In Nim they are integrated into the static type system. The official tutorial treats enums, ordinal types, subranges, and sets as basic advanced types.
In particular, range[0..100] is the Pascal-style idea of “expressing a value range as a type.”
9. Arrays are Pascal-like; sequences are Python-like
Fixed-length arrays:
var values: array[0..4, int]
values[0] = 10You can specify the index type of an array.
type
Weekday = enum
monday, tuesday, wednesday, thursday, friday
var hours: array[Weekday, int]
hours[monday] = 8This design is closer to Pascal’s arrays than C’s.
type
TWeekday = (Monday, Tuesday, Wednesday, Thursday, Friday);
THours = array[TWeekday] of Integer;Dynamic sequences:
var values: seq[int] = @[10, 20, 30]
values.add 40seq[T] is a variable-length contiguous container.
let names = @["Alice", "Bob", "Carol"]In feel, it is close to Python’s list.
names = ["Alice", "Bob", "Carol"]But Nim’s sequences have a fixed element type.
var values = @[1, 2, 3]
values.add "four" # compile errorIn summary:
- Fixed-length arrays with index types: Pascal-like
- Variable-length containers with easy literals: Python-like convenience
- Static element types: Nim’s static type system
10. Objects resemble Pascal records and C structs
type
Person = object
name: string
age: int
let person = Person(name: "Alice", age: 30)In Pascal, records:
type
TPerson = record
Name: String;
Age: Integer;
end;In Python, classes or dataclasses:
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: intImportantly, Nim’s object is a value type by default.
var a = Person(name: "Alice", age: 30)
var b = a
b.name = "Bob"
echo a.name # Alice
echo b.name # BobThis differs from ordinary Python class instances, where assignment makes both variables refer to the same object.
a = Person("Alice", 30)
b = a
b.name = "Bob"
print(a.name) # BobFor reference semantics, Nim uses ref object explicitly.
type
PersonRef = ref object
name: string
age: intMaking “value vs. reference” explicit in the type resembles C++, Pascal, and Rust more than Python.
11. Object inheritance exists, but the language is not class-centric
type
Person = ref object of RootObj
name: string
Employee = ref object of Person
employeeId: intInheritable base types usually use RootObj.
method describe(person: Person): string {.base.} =
"person: " & person.name
method describe(employee: Employee): string =
"employee: " & employee.nameNim distinguishes proc and method:
proc: static dispatchmethod: dynamic dispatch
Python methods normally live inside class definitions.
class Person:
def describe(self):
return f"person: {self.name}"In Nim, separating types and operations is idiomatic.
type
Person = object
name: string
proc describe(person: Person): string =
"person: " & person.nameThis resembles Pascal’s record + external procedures, C’s struct + functions, and Rust’s types + impl — unlike the “everything in a class” structure of Python or Java. The official tutorial also explains that Nim’s OOP is intentionally minimal and not the only design approach.
12. Method-call notation is UFCS
The following two mean the same thing.
add(values, 10)
values.add(10)Same for string operations.
let normalized = strip(toLowerAscii(name))can be written as:
let normalized = name.toLowerAscii.stripThis feature is commonly called UFCS (Uniform Function Call Syntax).
In Python,
name.lower().strip()looks similar, but the meaning differs: lower and strip fundamentally belong to the object. In Nim,
name.strip()is conceptually transformed into:
strip(name)This keeps data types and functions loosely coupled while allowing method-chain-like notation.
let result =
data
.filterIt(it > 0)
.mapIt(it * 2)It looks like a Python or JavaScript method chain, but the foundation is free functions. Nim is a language that provides an object-oriented appearance on top of a procedural function model.
13. Identifiers are case-sensitive only in the first letter, and underscores are ignored
Nim has an unusual name-comparison rule. Conceptually, the following names are identified:
parseHttpHeader
parse_http_headerExcept for the first character, case is also normalized, so Nim’s style insensitivity is strong. The standard style is camelCase.
proc parseHttpHeader() =
discardCallers can also write:
parse_http_header()However, the case of the first character matters.
Foo
fooThese are different identifiers.
This rule corresponds to none of Python, Pascal, or C. It is Nim’s own design for “absorbing notational differences in APIs.” The advantage is handling external libraries and C APIs with Nim-style names; the downside is that the compiler tolerates notational variation, which divides opinion regarding search and conventions.
14. for is Python-style iteration, but iterators expand statically
for value in values:
echo valueNearly the same appearance as Python.
for value in values:
print(value)Ranges are written as:
for i in 0..5:
echo i0..5 includes 5 (0, 1, 2, 3, 4, 5). The Python equivalent:
for i in range(0, 6):
print(i)For ranges excluding the end, use ..<.
for i in 0..<5:
echo iResult: 0, 1, 2, 3, 4.
You can define custom iterators.
iterator countdown(start: int): int =
var current = start
while current >= 0:
yield current
dec current
for value in countdown(3):
echo valueOutput:
3
2
1
0The yield looks like Python generators.
def countdown(start):
current = start
while current >= 0:
yield current
current -= 1But Nim’s inline iterators are normally zero-cost abstractions premised on compiler expansion. Do not assume the same execution model as Python generator objects. yield is the syntax that returns control from the iterator to the for body.
15. while and loop control are common ground of Pascal, C, and Python
var count = 0
while count < 10:
inc count
if count == 5:
continue
if count == 8:
breakwhile, break, and continue are the same as in Python and C-family. Some traditional Pascal implementations lacked continue, but modern Free Pascal and Delphi have it.
Uniquely convenient in Nim is the labeled block.
block search:
for row in matrix:
for value in row:
if value == target:
echo "found"
break searchYou can exit nested loops at once. In Python this may require flags, refactoring into functions, or exceptions.
found = False
for row in matrix:
for value in row:
if value == target:
found = True
break
if found:
breakNim’s block is closer to the labeled exits found in Ada and some structured languages than to Pascal’s named blocks.
16. Exception syntax is a hybrid of Python style and Pascal style
try:
riskyOperation()
except IOError as error:
echo error.msg
except ValueError:
echo "invalid value"
finally:
cleanup()Very similar to Python.
try:
risky_operation()
except OSError as error:
print(error)
except ValueError:
print("invalid value")
finally:
cleanup()Nim exception types are defined as:
type
ConfigurationError = object of CatchableErrorRaising:
raise newException(ConfigurationError, "invalid configuration")In Python:
class ConfigurationError(Exception):
pass
raise ConfigurationError("invalid configuration")The surface syntax is Python-like, but the static exception hierarchy and effect tracking are stricter than Python’s. A procedure’s raised exceptions can be described with a pragma.
proc loadConfig(): string {.raises: [IOError].} =
readFile("config.toml")This effect annotation is not exactly Java’s checked exceptions, but it is static side-effect information Python does not have.
17. defer resembles Go, Swift, and D
let file = open("data.txt")
defer:
file.close()
let content = file.readAll()file.close() runs when the current scope exits.
Python typically uses with:
with open("data.txt") as file:
content = file.read()Pascal typically uses try...finally:
FileStream := TFileStream.Create('data.txt', fmOpenRead);
try
...
finally
FileStream.Free;
end;Nim also has try/finally, but defer lets you write resource release next to acquisition.
let lock = acquireLock()
defer: releaseLock(lock)This is a style seen in Go, D, and Swift rather than Python or Pascal.
18. Generics are more concise than C++, with ML-style type constraints
proc first[T](values: openArray[T]): T =
values[0]
echo first([1, 2, 3])
echo first(["a", "b", "c"])Explicit instantiation also works.
echo first[int]([1, 2, 3])Type constraints:
proc double[T: SomeNumber](value: T): T =
value * 2Multiple type parameters:
proc convert[T, U](value: T; converter: proc(x: T): U): U =
converter(value)In being instantiated at compile time, Nim’s generics resemble C++ templates and Rust generics. Writing type parameters in [] is closer to C++, C#, and Scala than to Ada or Pascal-family generics.
Nim also has concepts.
type
Addable = concept x, y
x + y
proc combine[T: Addable](a, b: T): T =
a + bThis is a structural constraint similar to Go interfaces, Rust trait bounds, C++20 concepts, and Haskell type classes — though not identical to any of them.
19. distinct types push Pascal-style type safety further
type
UserId = distinct int
ProductId = distinct int
let userId = UserId(10)
let productId = ProductId(10)Both are represented as int, but they are different types.
proc loadUser(id: UserId) =
discard
loadUser(userId)
loadUser(productId) # compile errorPython type aliases are usually not fully distinct types, either at runtime or under static type checking.
UserId = int
ProductId = intPascal distinguishes types by declaration, but depending on the implementation and declaration form, they may be treated as aliases. Nim’s distinct is an explicit nominal type that shares representation while preventing mix-ups.
type
Meter = distinct float
Second = distinct floatUseful for preventing unit mix-ups as well.
20. Variant objects sit between Pascal’s variant records and algebraic data types
type
NodeKind = enum
integerNode
stringNode
Node = object
case kind: NodeKind
of integerNode:
intValue: int
of stringNode:
stringValue: stringUsage:
let number = Node(kind: integerNode, intValue: 42)
let text = Node(kind: stringNode, stringValue: "hello")
case number.kind
of integerNode:
echo number.intValue
of stringNode:
echo number.stringValuePascal also has variant records.
type
TNode = record
case Kind: TNodeKind of
nkInteger: (IntValue: Integer);
nkString: (StringValue: String);
end;Rust, by contrast, attaches values to enum variants.
enum Node {
Integer(i32),
String(String),
}Nim’s variant objects are syntactically very close to Pascal’s variant records, but because the correspondence between discriminator and fields is handled type-safely, in practice they can be used much like algebraic data types. The Nim compiler’s own official AST type is described as a variant object with case kind.
21. Distinguishing pointers from references is fundamentally different from Python
type
Node = ref object
value: int
next: Noderef is a managed reference.
var node = Node(value: 1)ptr is a low-level pointer.
var value = 10
var pointer: ptr int = addr value
echo pointer[]ref T: a reference under Nim’s memory managementptr T: an unmanaged pointer close to CT: the value itself
In Python these distinctions barely surface in the syntax. Pascal distinguishes values, class references, and pointers; C/C++ distinguish values and pointers. Here Nim is clearly on the systems-language side.
22. Operators can be defined as ordinary procedures
type
Vector = object
x, y: float
proc `+`(a, b: Vector): Vector =
Vector(x: a.x + b.x, y: a.y + b.y)
let a = Vector(x: 1, y: 2)
let b = Vector(x: 3, y: 4)
let c = a + bBackquotes treat operator names as identifiers. Custom operators can be defined too.
proc `***`(a, b: int): int =
a * a + b * b
echo 3 *** 4Python also has operator overloading, but via special methods such as __add__.
def __add__(self, other):
...C++ uses operator+. In Nim, operators are not special methods but ordinary overloadable procedures. This uniformity is also close to the thinking of functional languages and Lisp.
23. Command call syntax is shell/DSL-like
Ordinary call:
echo("hello", " ", "world")Parentheses can be omitted.
echo "hello", " ", "world"And combined with UFCS:
"hello".echoPython has no such syntax (you must write print("hello")). Pascal sometimes allows omitting parentheses, but not as generally as Nim.
In Nim, DSL-like notation becomes possible:
window:
title "Settings"
width 800
height 600This is usually realized in combination with templates and macros.
Omitting parentheses can obscure precedence, though.
echo abs -10In code like this you must understand how far the argument extends. For complex expressions, explicit parentheses are safer.
24. Templates are more type-safe than the C preprocessor and expand before functions
template twice(body: untyped) =
body
body
twice:
echo "hello"Conceptual expansion:
echo "hello"
echo "hello"Taking an expression:
template withLock(lock; body: untyped) =
acquire(lock)
try:
body
finally:
release(lock)
withLock myLock:
updateSharedState()This looks like a new control structure. Compared with C macros:
#define TWICE(x) do { x; x; } while (0)templates are integrated with the AST, scoping, and type information.
Python decorators and higher-order functions cannot achieve the same syntactic extension.
with lock:
update_shared_state()Python’s with is built-in language syntax; users do not freely create new grammatical blocks. In Nim, templates allow libraries to provide APIs close to language syntax.
25. Macros are Lisp-like, but the surface syntax is ordinary Nim
import std/macros
macro debug(expression: untyped): untyped =
result = quote do:
echo `expression`.astToStr, " = ", `expression`
let value = 42
debug valueA macro receives Nim’s AST at compile time and returns another AST. The official tutorial defines macros as “functions executed at compile time that transform a Nim syntax tree into a different tree.”
Like Lisp’s (defmacro ...), Rust’s macro_rules! ..., and Elixir’s defmacro ..., this is metaprogramming that treats code as data.
But whereas Lisp code is itself S-expressions, Nim converts its ordinary indentation syntax into an AST.
dumpTree:
var person = Person(name: "Alice", age: 30)dumpTree lets you inspect the syntax tree. The official macro tutorial explains that a code block can be passed as a macro’s trailing argument and that dumpTree reveals the AST representation.
Hence Nim can be described as having Lisp-grade syntax manipulation machinery underneath a readable Python-like surface.
26. Compile-time execution is broader than C++ constexpr
func factorial(n: int): int =
if n <= 1:
1
else:
n * factorial(n - 1)
const value = factorial(10)factorial(10) is computed at compile time.
An explicit compile-time block:
static:
echo "executed during compilation"Compile-time conditionals:
when defined(windows):
echo "Windows"
elif defined(linux):
echo "Linux"
else:
echo "other"when is not a runtime if. Branches not chosen are normally not compiled.
when sizeof(int) == 8:
type NativeWord = int64
else:
type NativeWord = int32The official manual explains that constant expressions, macro definitions, and Nim procedures called from macros can run at compile time, with semantic analysis and compile-time execution proceeding interleaved. This is territory integrating elements of:
- C’s preprocessor
- C++‘s
constexpr - D’s CTFE
- Zig’s
comptime - Lisp’s macros
27. Pragmas sit between Pascal directives and C attributes
proc importedFunction(x: cint): cint
{.importc, header: "<example.h>".}proc fastOperation() {.inline.} =
discardtype
PackedHeader {.packed.} = object
version: uint8
flags: uint8Pragmas are enclosed in {. and .}. The official tutorial describes pragmas as a mechanism for giving the compiler extra information and commands without adding a large number of keywords.
Pascal compiler directives:
{$IFDEF WINDOWS}C/C++ attributes:
[[nodiscard]]Rust attributes:
#[derive(Debug)]play a similar role. But in Nim, FFI, optimization, effects, memory representation, exports, and many other language features are unified under pragmas.
28. Public module symbols are marked explicitly with *
type
Person* = object
name*: string
age: int
proc newPerson*(name: string; age: int): Person =
Person(name: name, age: age)Only symbols marked with * are exported to other modules.
Person*: publicname*: publicage: privatenewPerson*: public
Python uses the leading-underscore convention.
_internal_value = 10Pascal separates visibility via interface and implementation sections. Nim attaches visibility per declaration — like Rust’s pub compressed into a postfix marker.
pub struct Person {
pub name: String,
age: u32,
}The * is short but easily mistaken for a pointer operator; in Nim it is an export marker.
29. Imports look Python-like, with strong selective control
import std/strutilsMultiple:
import std/[strutils, sequtils, tables]Specific symbols:
from std/strutils import strip, toLowerAsciiAlias:
import std/strutils as stringsExclusion:
import std/strutils except splitSimilar to Python:
import pathlib
from pathlib import PathMore flexible than Pascal’s uses:
uses
SysUtils, Classes;Nim combines module paths, selective imports, exclusion, and aliasing at the language level.
30. Comments are Python-style; nestable multi-line comments are distinctive
# single-line commentSame as Python.
Documentation comments:
proc add(a, b: int): int =
## Adds two integers.
a + bMulti-line comments:
#[
multi-line comment
]#They even nest:
#[
outer
#[
inner
]#
outer continues
]#The official manual specifies the #[ ... ]# multi-line comment form and its nesting. C’s /* ... */ does not nest in the standard. Python has no dedicated multi-line comment syntax. Pascal’s { ... } and (* ... *) nesting rules vary by implementation.
31. Strings add systems-language representations to Python-like convenience
Ordinary strings:
let message = "hello"Multi-line strings:
let sql = """
select *
from users
where active = true
"""Raw strings:
let path = r"C:\Users\name\data"Generalized raw strings:
let regex = re"\d+\.\d+"The re in re"..." is not a special grammar keyword but a generalized prefix call on a string literal.
String interpolation comes from a standard-library macro.
import std/strformat
let name = "Alice"
let age = 30
echo fmt"{name} is {age} years old"Python:
print(f"{name} is {age} years old")They look similar, but Nim’s fmt is not built into the language — it is a compile-time macro. The official macro tutorial cites strformat as a practical macro example.
Nim’s design philosophy shows here: rather than hard-coding every convenient notation into the compiler, macros let libraries implement them.
32. Type conversion uses Python-like call notation, but implicit conversion is restrictive
let integerValue = int(3.14)
let floatValue = float(10)Looks like Python.
integer_value = int(3.14)
float_value = float(10)But Nim is statically typed, so convertibility is checked at compile time.
There are also casts.
let bits = cast[uint32](someFloat)Nim separates ordinary conversion (int(value)) from low-level reinterpretation (cast[int](value)). The syntax combines C-style casts, Pascal type casts, and Python constructor-style conversion.
33. nil is not Python’s None; it is a pointer-like null
var node: ref Node = nil
if node.isNil:
echo "no node"Python:
node = None
if node is None:
print("no node")They look similar, but Nim’s nil is a null value for references, pointers, and some procedural types. It is not a universal “no value” assignable to any value type.
To express presence/absence safely, use Option[T].
import std/options
proc findUser(id: int): Option[string] =
if id == 1:
some("Alice")
else:
none(string)This is close to Rust’s Option<T> and Haskell’s Maybe.
34. Type-class-like overload resolution
proc describe(value: int): string =
"integer: " & $value
proc describe(value: string): string =
"string: " & value
echo describe(42)
echo describe("hello")Same-named procedures can be overloaded by type. In Python, redefining a function of the same name normally overwrites it.
def describe(value: int):
...
def describe(value: str):
...In standard Python these do not coexist as two implementations.
Pascal, C++, and Java have overloading. Nim further combines it with UFCS and generics.
echo 42.describe
echo "hello".describeOverloading + UFCS + type inference lets you build polymorphic APIs without class hierarchies.
35. func is a procedure with side-effect constraints
func square(value: int): int =
value * valueNim has func alongside proc.
proc logAndSquare(value: int): int =
echo value
value * valuefunc is used as a procedure with restricted side effects.
func logAndSquare(value: int): int =
echo value # rejected as a side effect
value * valuePython has no language-level purity marker. Nim is not fully pure-functional like Haskell, but it can restrict side effects per procedure.
Nim clearly separates:
- Ordinary processing:
proc - Side-effect-restricted processing:
func - Value-producing iteration:
iterator - Syntax expansion:
template - AST transformation:
macro
as distinct declaration keywords. This classification is an important Nim-specific trait.
36. Putting Nim’s syntax together in one example
import std/[options, strformat]
type
UserId = distinct int
Role = enum
guest
member
administrator
User = object
id: UserId
name: string
role: Role
proc displayName(user: User): string =
case user.role
of guest:
fmt"Guest {user.name}"
of member:
user.name
of administrator:
fmt"Administrator {user.name}"
proc findUser(id: UserId): Option[User] =
if id == UserId(1):
some User(
id: id,
name: "Alice",
role: administrator
)
else:
none(User)
let userId = UserId(1)
case findUser(userId)
of Some(@user):
echo user.displayName
of None:
echo "User not found"Note that the pattern syntax on Option at the end should be verified against the library, Nim version, and usage. The more standard way is:
let found = findUser(userId)
if found.isSome:
let user = found.get
echo user.displayName
else:
echo "User not found"Organizing which style each construct in this example is closest to:
| Construct | Closest style |
|---|---|
| Indentation blocks | Python / off-side rule |
import | Python-like |
type sections | Pascal/Modula-like |
Name: Type | Pascal-like |
distinct int | Strong static typing, distinctly Nim |
enum | Pascal-family + modern typed languages |
object | Pascal record / C struct |
proc | Pascal/Modula |
| Trailing-expression returns | ML/Rust/Scala-like |
case ... of | Pascal-like |
if as expression | ML/Rust-like |
Option[T] | ML/Haskell/Rust-like |
user.displayName | UFCS |
fmt"..." | Extension via AST macro |
A Precise Breakdown: Which Is Nim More Similar To — Python or Pascal?
Features similar to Python
Syntactically, the clear similarities to Python are:
- Blocks formed by indentation
if/elif/elsefor x in collectionwhiletry/except/finallyraiseimport/from ... import#comments[]container notation- Concise function calls
- A surface syntax prioritizing readability
However, the following are not similar to Python:
- Dynamic typing
- Runtime design centered on duck typing
- The everything-is-an-object model
- Class-centric method design
- Metaprogramming centered on runtime reflection
- An execution model premised on an interpreter
- The same iterator implementation as Python generators
In short: Nim resembles Python in look and feel, but not in execution model.
Features similar to Pascal
The strong similarities to Pascal are:
var,const,type- The procedural vocabulary
proc name: Typetype annotations- Grouping same-typed parameters in declarations
case ... ofenum- Subrange types
- Set types
- Arrays with specifiable index types
objectclose to records- Variant objects close to variant records
- The distinction between value types and reference types
- A compiled language
- Static data representation mindful of proximity to C
However, the following are not similar to Pascal:
begin/end- Mandatory statement-ending semicolons
:=for assignment=for comparisonthendo- Returning values by assigning to the function name
- Strict separation of declaration and execution sections
- The
program ... begin ... end.form
In short: Nim resembles Pascal’s style of types and declarations while lacking Pascal’s verbose delimiters.
Nim’s core is not “Python + Pascal” but an extensible grammar
Understanding Nim only as a midpoint between Python and Pascal misses the most important part. Nim’s core is the combination of:
A unified indentation-based syntax
+
Static typing and value representation
+
Overloading and UFCS
+
template
+
AST macro
+
compile-time executionFor example, the following code looks like user-invented syntax:
benchmark "database lookup":
let result = database.find(key)
check result.isSomeIf benchmark and check are templates or macros, a library can provide a DSL close to control syntax.
Python cannot add new syntax, so it leans on functions, decorators, and context managers:
with benchmark("database lookup"):
result = database.find(key)
assert result is not NonePascal is even more grammatically constrained. Nim keeps the surface syntax small and lets libraries manipulate the AST to build domain-specific constructs. Official materials state that macros transform the AST at compile time and can take arbitrary code blocks as arguments.
Conclusion
The key points of this article:
If Nim had to be described in one sentence, this is the most accurate:
A language in which Pascal-style declarations, types, and value representation are written in Python-style indentation syntax, integrating C-style low-level control, ML-style expression orientation, and Lisp-style AST metaprogramming.
This article’s classification is not about historical adoption — which language something was taken from — but about how similar the syntax and semantics are, based on the official specification. For the person writing code, what matters is not lineage but what style of syntax it is.
Nim’s essence lies less in “writing like Python” than in reducing syntactic friction without losing static meaning. Where Python used indentation to make a dynamic language concise, Nim uses indentation to make Pascal/C-grade types and systems control concise. And Nim’s center of gravity is its extensible grammar via templates, AST macros, and compile-time execution — the “Python + Pascal” framing alone misses the most important part.
As background, the block designs we confirmed: the biggest difference between Pascal and Python is how blocks are represented. Pascal’s begin/end creates blocks with tokens, and indentation is merely “formatting for humans” ignored by the compiler. Python’s indentation is a “syntactic symbol” converted into INDENT/DEDENT tokens during lexing. This “indentation as syntax” design is called the off-side rule and is adopted by Haskell, F#, Elm, CoffeeScript, and Nim — but the designs are not monolithic. In Haskell the layout rule is syntactic sugar for {} and ; with an explicit fallback notation, while Python has no block representation other than indentation, and Nim has its own setup: tabs forbidden, no {} alternative.
For an introduction to Nim itself and its real-world performance, see our previous article “Getting Started with Nim: A Multi-Paradigm Language That Transpiles to C”.