asopi tech
asopi techIndie Developer
Deep Insights into Nim Syntax — How Similar Is It to Pascal, Python, and Haskell?

[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.

  1. A syntax comparison of Pascal and Python (Part 1)
  2. The difference between “Pascal’s indentation is formatting; Python’s indentation is syntax” (Part 2)
  3. An overview of languages where indentation forms blocks (off-side rule languages) (Part 3)
  4. Haskell’s layout rule as syntactic sugar (Part 4)
  5. 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:

ItemPascalPython
Comments{...} or (*...*)#
Variable declarationvar x: Integer;x = 0
Constantsconst PI = 3.14;PI = 3.14 (convention)
Assignmentx := 10;x = 10
Comparison===
Blocksbegin ... endindentation
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 += 1

Functions

function Add(a, b: Integer): Integer;
begin
  Add := a + b;
end;
def add(a, b):
    return a + b

In 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] = 10

Pascal 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: int

Classes

type
  TPerson = class
  public
    procedure Hello;
  end;
class Person:
    def hello(self):
        print("Hello")

Major syntactic differences

PascalPython
Blocks via begin/endBlocks via indentation
Assignment with :=Assignment with =
= is comparison== is comparison
Static typingDynamic typing (type hints optional)
Declarations requiredNo declarations
Compiled languagePrimarily 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/end pair 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 -x

Haskell 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 * x

A 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, and proc
  • 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 + y

Explicit version:

let {
  x = 1;
  y = 2
} in x + y

Once you write {}, indentation carries no syntactic meaning inside that block.

let {
x = 1;
        y = 2
      } in x + y

Even 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 do block is managed by indentation
  • The let block 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

shutdown

Whereas 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 allowed

Also, 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 = 3
var
  count: Integer;
  name: String;

const
  MaxRetries = 3;
count = 10
name = "Alopex"
MAX_RETRIES = 3

Nim’s declarations are Pascal-style, with the order name : type.

var count: int

C-family is the reverse.

int count;

Nim also distinguishes var, let, and const.

var mutableValue = 1
let immutableValue = 2
const compileTimeValue = 3
DeclarationMeaning
varReassignable variable
letValue not reassignable after initialization
constConstant 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    # bool

This differs from Python’s dynamic typing.

var value = 10
value = "ten"        # compile error

The 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 = 10
x = 10
x := 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 + b
function Add(a, b: Integer): Integer;
begin
  Add := a + b;
end;
def add(a: int, b: int) -> int:
    return a + b

Nim 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 + b

Early 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 =
  discard

All 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) =
  discard

Commas work too.

proc connect(host: string, port: int, secure: bool) =
  discard

Using 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:
    -x

This 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
    failed

Subrange types:

type
  Percentage = range[0..100]

var progress: Percentage = 80

Set 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] = 10

You can specify the index type of an array.

type
  Weekday = enum
    monday, tuesday, wednesday, thursday, friday

var hours: array[Weekday, int]
hours[monday] = 8

This 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 40

seq[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 error

In 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: int

Importantly, 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  # Bob

This 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)  # Bob

For reference semantics, Nim uses ref object explicitly.

type
  PersonRef = ref object
    name: string
    age: int

Making “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: int

Inheritable base types usually use RootObj.

method describe(person: Person): string {.base.} =
  "person: " & person.name

method describe(employee: Employee): string =
  "employee: " & employee.name

Nim distinguishes proc and method:

  • proc: static dispatch
  • method: 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.name

This 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.strip

This 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_header

Except for the first character, case is also normalized, so Nim’s style insensitivity is strong. The standard style is camelCase.

proc parseHttpHeader() =
  discard

Callers can also write:

parse_http_header()

However, the case of the first character matters.

Foo
foo

These 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 value

Nearly the same appearance as Python.

for value in values:
    print(value)

Ranges are written as:

for i in 0..5:
  echo i

0..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 i

Result: 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 value

Output:

3
2
1
0

The yield looks like Python generators.

def countdown(start):
    current = start
    while current >= 0:
        yield current
        current -= 1

But 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:
    break

while, 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 search

You 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:
        break

Nim’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 CatchableError

Raising:

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 * 2

Multiple 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 + b

This 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 error

Python type aliases are usually not fully distinct types, either at runtime or under static type checking.

UserId = int
ProductId = int

Pascal 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 float

Useful 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: string

Usage:

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.stringValue

Pascal 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: Node

ref 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 management
  • ptr T: an unmanaged pointer close to C
  • T: 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 + b

Backquotes treat operator names as identifiers. Custom operators can be defined too.

proc `***`(a, b: int): int =
  a * a + b * b

echo 3 *** 4

Python 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".echo

Python 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 600

This is usually realized in combination with templates and macros.

Omitting parentheses can obscure precedence, though.

echo abs -10

In 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 value

A 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 = int32

The 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.} =
  discard
type
  PackedHeader {.packed.} = object
    version: uint8
    flags: uint8

Pragmas 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*: public
  • name*: public
  • age: private
  • newPerson*: public

Python uses the leading-underscore convention.

_internal_value = 10

Pascal 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/strutils

Multiple:

import std/[strutils, sequtils, tables]

Specific symbols:

from std/strutils import strip, toLowerAscii

Alias:

import std/strutils as strings

Exclusion:

import std/strutils except split

Similar to Python:

import pathlib
from pathlib import Path

More 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 comment

Same as Python.

Documentation comments:

proc add(a, b: int): int =
  ## Adds two integers.
  a + b

Multi-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".describe

Overloading + 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 * value

Nim has func alongside proc.

proc logAndSquare(value: int): int =
  echo value
  value * value

func is used as a procedure with restricted side effects.

func logAndSquare(value: int): int =
  echo value   # rejected as a side effect
  value * value

Python 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:

ConstructClosest style
Indentation blocksPython / off-side rule
importPython-like
type sectionsPascal/Modula-like
Name: TypePascal-like
distinct intStrong static typing, distinctly Nim
enumPascal-family + modern typed languages
objectPascal record / C struct
procPascal/Modula
Trailing-expression returnsML/Rust/Scala-like
case ... ofPascal-like
if as expressionML/Rust-like
Option[T]ML/Haskell/Rust-like
user.displayNameUFCS
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/else
  • for x in collection
  • while
  • try/except/finally
  • raise
  • import/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: Type type annotations
  • Grouping same-typed parameters in declarations
  • case ... of
  • enum
  • Subrange types
  • Set types
  • Arrays with specifiable index types
  • object close 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 comparison
  • then
  • do
  • 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 execution

For example, the following code looks like user-invented syntax:

benchmark "database lookup":
  let result = database.find(key)
  check result.isSome

If 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 None

Pascal 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”.