Hica vs Python
If you’re looking for a first programming language, whether for yourself, your kids, or your students, Python and hica are both excellent choices. But they take very different paths to the same goal: making programming accessible, with different trade-offs between simplicity, safety, and explicitness.
At a Glance
| Dimension | Python | hica |
|---|---|---|
| Type safety | Dynamic typing (errors at runtime unless using type hints) | Compile-time errors |
| Readability | Excellent (indentation) | Excellent (arrows, braces) |
| Mutability | Mutable by default | Immutable by design |
| Functions | def + simple lambdas (single-expression only) |
fun + full closures + |> pipe |
| Error handling | Exceptions (implicit flow) | Result types + combinators (explicit handling) |
| Data structures | Classes / dataclasses | Structs + enums |
| Dictionaries | dict (built-in, mutable) |
Maps ({"k": v}, immutable list of tuples) |
| Lists | List comprehensions | map/filter/fold + pipe |
| Pattern matching | Added in 3.10, optional | Core feature from day one |
| Loops | for, while, break, continue |
for, while, repeat, loop, break, continue |
| Performance | Interpreted (generally slower) | Compiled to C (generally faster) |
| Ecosystem | Massive | Small but growing |
Type Safety
Python is dynamically typed. Errors like adding a number to a string only show up when the code runs:
def greet(name):
return "Hello, " + name
greet(42) # TypeError at runtime
hica catches most type errors at compile time:
fun greet(name) => "Hello, " + name
fun main() {
greet(42) // Compile error: expected string, got int
}
Readability
Python:
def double(x):
return x * 2
result = double(21)
print(result)
hica:
fun double(x) => x * 2
fun main() {
let result = double(21)
println(result)
}
Both are very readable. Python wins on prose-like syntax; hica wins on explicitness: curly braces make nesting clear, let makes bindings visible.
Immutability
Python has mutable variables by default:
scores = [85, 92, 78]
scores.append(95) # mutates the original list
scores[0] = 100 # changes in place
hica defaults to immutable with let. You create new values instead of mutating:
let scores = [85, 92, 78]
let updated = scores + [95]
let doubled = map(scores, (x) => x * 2)
When you need mutation, use var:
var count = 0
while count < 10 {
println(count)
count = count + 1
}
var is locally scoped and effect-safe — the mutable state can’t leak outside the function. This gives you the convenience of mutation when you need it, without the risks of shared mutable state.
Functions and Closures
Python has def and simple lambdas (single-expression only):
double = lambda x: x * 2
scores = list(map(double, [1, 2, 3, 4, 5]))
hica has fun and full closures with the pipe operator:
let double = (x) => x * 2
let scores = [1, 2, 3, 4, 5] |> map((x) => x * 2)
Error Handling
Python uses exceptions:
def safe_divide(a, b):
if b == 0:
raise ValueError("division by zero")
return a / b
try:
result = safe_divide(10, 0)
except ValueError as e:
print(e)
hica uses Result types. The compiler forces you to handle both cases:
fun safe_divide(a, b) =>
if b == 0 { Err("division by zero") }
else { Ok(a / b) }
fun main() {
match safe_divide(10, 0) {
Ok(n) => println(n),
Err(e) => println(e)
}
}
For chaining, hica has combinators that reduce the verbosity:
fun main() {
let result = safe_divide(10, 2)
|> map_result((n) => n * 10) // Ok(50)
|> and_then_result((n) => safe_divide(n, 5)) // Ok(10)
println(result)
}
For functions that chain several fallible steps, the ? operator keeps things flat:
fun add_strings(a: string, b: string) : maybe<int> {
let x = parse_int(a)? // None → return None early
let y = parse_int(b)?
Some(x + y)
}
This makes error paths explicit. Python’s exception model is more concise for simple cases, but errors can silently propagate. hica forces you to handle each failure point, either with match, combinators like map_result, or the ? operator.
String Operations
Python has extensive string methods built into the str type:
msg = " Hello, World! "
print(msg.strip()) # "Hello, World!"
print(msg.upper()) # " HELLO, WORLD! "
print("World" in msg) # True
print("a,b,c".split(",")) # ['a', 'b', 'c']
print(", ".join(["a", "b"])) # "a, b"
print(msg.replace("World", "Python")) # " Hello, Python! "
hica has the same operations as free functions:
fun main() {
let msg = " Hello, World! "
println(trim(msg))
println(to_upper(msg))
println(contains(msg, "World"))
println(split("a,b,c", ","))
println(join(["a", "b"], ", "))
println(replace(msg, "World", "hica"))
}
Indexing & slicing
| Python | hica | Result |
|---|---|---|
s[0] |
s[0] |
First character (hica returns char) |
s[-1] |
s[-1] |
Last character |
s[1:4] |
s[1:4] |
Substring (same syntax!) |
s[:3] |
s[:3] |
First 3 characters |
s[2:] |
s[2:] |
From index 2 to end |
s.capitalise() |
capitalise(s) |
"hello" → "Hello" |
s.removeprefix("v") |
removeprefix(s, "v") |
Strip prefix |
s.removesuffix(".txt") |
removesuffix(s, ".txt") |
Strip suffix |
Python uses method syntax (s.strip()), hica uses function syntax (trim(s)) — but hica also supports dot-call syntax, so you can write s.trim().to_upper() just like method chaining. The pipe operator works too: s |> trim |> to_upper. Both styles are equivalent; use whichever you prefer.
Parsing & Type Conversion
Python uses built-in constructors that raise exceptions on bad input:
n = int("42") # 42
f = float("3.14") # 3.14
int("abc") # ValueError at runtime
hica has safe parse functions that return maybe instead of crashing:
fun main() {
println(parse_int("42")) // Some(42)
println(parse_int("abc")) // None
println(parse_float("3.14")) // Some(3.14)
println(parse_float("xyz")) // None
match parse_int("100") {
Some(n) => println("Got: {n}"),
None => println("Not a number")
}
}
Python’s int() / float() are concise but require try/except for safety. hica’s parse_int / parse_float make the failure case explicit through maybe, so invalid input can never crash.
Custom Data Types
Python uses classes or dataclass to define custom types:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(3, 4)
print(p.x) # 3
print(p) # Point(x=3, y=4)
hica uses struct:
struct Point { x: int, y: int }
fun main() {
let p = Point { x: 3, y: 4 }
println(p.x) // 3
println(p) // Point(x: 3, y: 4)
}
Python has classes with inheritance, methods, and dunder protocols. hica has simple immutable structs with free functions, no self, __init__ or inheritance. Functions that operate on structs are just regular functions:
struct Point { x: int, y: int }
fun distance_sq(p: Point) : int => p.x * p.x + p.y * p.y
Enums (Algebraic Types)
Python doesn’t have built-in algebraic types. You can approximate them with classes or enum.Enum, but there’s no exhaustiveness checking and no variant data:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
# For variants with data, you need classes:
class Circle:
def __init__(self, radius):
self.radius = radius
class Rect:
def __init__(self, w, h):
self.w = w
self.h = h
def area(shape):
if isinstance(shape, Circle):
return 3.14159 * shape.radius ** 2
elif isinstance(shape, Rect):
return shape.w * shape.h
# Easy to forget a case — no compiler warning!
hica has first-class enum types with exhaustiveness checking:
type Shape {
Circle(radius: float),
Rect(width: float, height: float),
Point
}
fun area(s: Shape) : float => match s {
Circle(r) => 3.14159 * r * r,
Rect(w, h) => w * h,
Point => 0.0
}
If you forget a variant, the compiler warns you:
warning: non-exhaustive match: missing Point
Python’s isinstance chains are error-prone and have no compile-time safety. hica’s match is exhaustive — every case must be handled. This is one of hica’s strongest advantages over Python for modelling data that comes in different shapes.
Dictionaries / Maps
Python has a built-in mutable dict type:
ages = {"kalle": 30, "olle": 25}
ages["lisa"] = 35 # mutates in place
print(ages["kalle"]) # 30
print(ages.get("nobody")) # None
del ages["olle"]
print(list(ages.keys())) # ['kalle', 'lisa']
hica has map literals with the same {"key": value} syntax, but maps are immutable lists of tuples:
fun main() {
let ages = {"kalle": 30, "olle": 25}
let ages2 = ages.map_set("lisa", 35)
println(ages2.map_get("kalle")) // Just(30)
println(ages2.map_get("nobody")) // Nothing
let ages3 = ages2.map_remove("olle")
println(ages3.map_keys()) // ["kalle", "lisa"]
}
| Python | hica |
|---|---|
d[key] (raises KeyError) |
map_get(m, key) (returns maybe) |
d[key] = val (mutates) |
map_set(m, key, val) (returns new map) |
del d[key] |
map_remove(m, key) |
d.keys() |
map_keys(m) |
d.values() |
map_values(m) |
key in d |
map_contains_key(m, key) |
len(d) |
map_size(m) |
Python dicts are mutable hash tables with O(1) lookup. hica maps are immutable association lists — simpler and composable with all list functions (filter, map, fold), but O(n) lookup. For small maps this is fine; for large data sets, Python’s dict is more efficient.
Pattern Matching
Python added match in 3.10, but it’s optional:
match command:
case "quit":
exit()
case _:
print("Unknown")
hica makes match central. It works with integers, strings, Some/None, Ok/Err, wildcards, or-patterns, range patterns, and struct destructuring:
fun describe(x) => match x {
0 => "nothing",
1 | 2 => "few",
_ => "many"
}
fun grade(score: int) => match score {
0..=59 => "F",
60..=69 => "D",
70..=79 => "C",
80..=89 => "B",
90..=100 => "A",
_ => "invalid"
}
struct Point { x: int, y: int }
fun classify(p: Point) : string => match p {
Point { x: 0, y: 0 } => "origin",
Point { x, y } => "({x}, {y})"
}
hica also supports list slice patterns for recursive processing:
fun sum(xs: list<int>) : int => match xs {
[] => 0,
[x, ..rest] => x + sum(rest)
}
Python’s match supports guards (case x if x > 0) but has no range pattern syntax or list slice patterns. hica’s ..= makes numeric ranges concise and [x, ..rest] provides idiomatic list destructuring. Both languages support struct/class destructuring in patterns.
Loops
Python has for, while, break, and continue:
for x in [1, -2, 3]:
if x < 0:
continue
print(x)
while True:
line = input()
if line == "quit":
break
hica has the same constructs plus repeat and loop:
for x in [1, -2, 3] {
if x < 0 { continue }
println(x)
}
loop {
// runs forever until break
break
}
repeat(3) {
println("tick")
}
break and continue work in all loop types: while, for, repeat, and loop. Python’s for/else and while/else have no equivalent in hica.
Bitwise Operations
Python uses infix operators for bitwise operations:
flags = 0b1010_1100
masked = flags & 0x0F # AND
shifted = flags >> 4 # shift right
flipped = flags ^ 0xFF # XOR
complement = ~flags # NOT
hica uses named functions instead of operators:
fun main() {
let flags = 0b1010_1100
let masked = bit_and(flags, 0x0F)
let shifted = bit_shr(flags, 4)
let flipped = bit_xor(flags, 0xFF)
let complement = bit_not(flags)
}
Python’s operators are concise; hica’s named functions are explicit and self-documenting. Both support binary (0b) and hex (0x) literals with underscore separators (0b1111_0000).
hica also supports bit-level pattern matching with ? wildcards, which Python has no equivalent for:
match byte {
0b1100_???? => "high nibble is C",
_ => "other"
}
Performance
Python is interpreted. hica compiles through Koka to C, so the resulting binaries can run at native speed for many workloads.
Ecosystem
Python has an enormous ecosystem: NumPy, pandas, Django, thousands of tutorials, and answers for every question. hica is new with a growing set of examples and the Koka standard library underneath. Python wins decisively here.
Conclusion
Python is the safe, proven choice with the largest ecosystem and lowest barrier to entry.
hica emphasises foundations like immutability, type safety, pattern matching, and explicit error handling. Students who learn hica carry these patterns into Python, Rust, TypeScript, or whatever they use next.
Why not both? Start with hica to build the foundations, then move to Python with a head start on the concepts that matter most.