I’ve been experimenting with Mojo for a few days and have really enjoyed learning the language so far. While exploring variable declarations, I encountered a behavior that surprised me.
According to the documentation:
“Variables explicitly declared with var have block-level scope.”
“This is in contrast to implicitly-declared variables (those without the var keyword), which use function-level scoping (consistent with Python variable behavior).”
What surprised me is that the scope of a variable depends on how it is declared. For example, two variables declared in similar locations can have different lifetimes and visibility depending on whether var is used.
As someone coming from Python, I expected the declaration style and the scope rules to be independent concepts. Personally, I would find it easier if both explicit and implicit declarations followed the same scoping rules, whether that rule is block scope or function scope.
I understand the rationale behind encouraging explicit declarations. In fact, var can help prevent accidental shadowing and make code easier to reason about. Because of that, I wonder whether it would make sense to eventually encourage explicit declarations more strongly and unify the scoping model.
This isn’t a major issue, and it’s certainly something users can learn. However, the difference in scope based solely on declaration style was unexpected enough that I initially thought I had misunderstood the language.
I’d be interested to hear the reasoning behind this design choice. Thanks for all the great work on Mojo—I’m looking forward to learning the language in greater depth.
As the docs say, the behaviour of the implicit declaration variable is designed to be consistent with Python. In fact, I suspect the only reason this way of declaring variables exists is to maintain consistency with Python. it would make migration easy without introducing correctness issues in code that already worked in Python.
Possibly, though it didn’t actually make migration easier for me. The var declaration feels inconsistent both with Python’s scoping rules and with Mojo’s own implicit declaration behavior. Personally, I’d prefer the language to prioritize internal consistency over Python compatibility here.
Most Python developers would probably understand “Mojo uses block scopes instead of function scopes” pretty quickly. Overall the experience has been fantastic so far and the docs are excellent — this is just one small paper cut I’d love to see improved."
There were some lengthy discussions related to this and there were two fractions in the community:
require explicit block scoped var declaration:
makes Mojo less complex
prohibits accidental introduction of vars (e.g color vs. colour)
no accidental var overrides
easier to reason about the code/LLM friendly
…
allow implicit function scoped variable declarations:
more „pythonic“, keeps Mojo closer to Python
good for scripting: less to write
in reality there are no real issues with this
…
IMO both have their pros and cons although I personally lean towards the “require explicit var” fraction.
I think at the end it‘s more of a style guide thing. Maybe this could be the starting point for a mojo lint feature. This way one could decide on a project base what style should be enforced with a linting rule.
AFAIK your proposal to make implicit variable declarations having the same scoping rules as var declarations was not discussed in the community. Not sure if this would be a good “compromise”:
less complex: all variables have the same scoping rules (e.g. block scoped)
more „pythonic" (allow implicit variable declarations although with changed semantics if block scoped)
I appreciate you taking the time to share this thoughtful summary — it’s very helpful. I understand that language design is full of trade-offs, and keeping Mojo approachable for Python developers is clearly a high priority.
Requiring the var keyword for explicit declaration reduces accidental bugs (like typos or shadowing) and improves readability, and makes the code easier to reason about — both for humans and tools. That said, I also see the appeal of the implicit-but-block-scoped approach as a potential middle ground. It could preserve some of the “Pythonic” feel while bringing the important consistency of block scoping.
Either way, your suggestion about making this configurable via linting rules sounds like an excellent idea. It would let teams choose the style that best fits their project without forcing a single choice on the whole community.
Thanks again for the detailed feedback and for pointing out the earlier discussions — I wasn’t aware of how much this had already been debated.
I completely agree — the current mix feels like a legacy compromise rather than a clean design choice.
Go handled implicit declarations elegantly with the walrus operator (:=). It clearly signals “this is a new variable” while keeping the same scoping rules as explicit var declarations. No surprises.
Python’s walrus operator (:=) is more limited (mostly for expressions), but Mojo has the opportunity to do better:
Proposal: Make the walrus operator (:=) mandatory for all implicit-style (type-inferred) declarations, everywhere.
Benefits:
Much clearer visual distinction between declaration and reassignment → fewer accidental overwrites/shadowing bugs.
More internally consistent than Python.
No real surprises for python developers who already know the Walruss operator.
This would feel like a thoughtful evolution rather than carrying forward Python’s implicit declaration quirks just for compatibility.
What do you think? Has anyone from the Modular team commented on introducing := for implicit declarations?
Thanks for the quick reply — I wasn’t aware of Mojo’s walrus operator.
while (name := input("Name or ‘quit’: ")) != “quit”:
print(“Hello,”, name)
This is exactly how the walrus operator is used in Python.
My main point is that this creates an implicit declaration of name, which feels inconsistent with the usual = assignment rule. It’s a well-known quirk inherited from Python.
Suggestion:
Make the walrus operator (:=) the standard for all implicit (type-inferred) declarations — not just inside expressions. For example:
x := 42 → new variable (declaration + inference)
x = value → always reassignment
var x = → explicit style remains available
This would:
Remove the distinction in scoping behavior between implicit and explicit declarations.
Reduce accidental shadowing / overwriting bugs.
Make Mojo feel more consistent and modern while staying friendly to Python developers (who already know := from Python).
It builds on what already exists instead of carrying forward Python’s legacy quirks. What do you think?
By the way this is a similar proposal, but apparently, he wants to drop var, which is not a good idea. You should be able to declare a variable without assigning a value.
This is a great observation, Kilian, and it hits on one of the most critical design paradigms in Mojo right now.
Perhaps a helpful and positive way to frame this behavior is to view the var keyword not just as a syntax requirement, but as an optimization constraint.
When we omit var, we are essentially asking the compiler to handle the scope defensively. To preserve Python compatibility, the compiler has to stretch that variable’s visibility across the wider function scope, keeping references alive longer than may actually be necessary.
By explicitly adding var, you are actively taking control of the compiler’s lifetime analysis pass. You are saying: ‘I am locking this variable scope down to this exact block.’
This makes the compiler’s optimization job infinitely simpler. The moment the block closes, Mojo can definitively inject the destructor (__del__) and immediately free up hardware memory or GPU resources, rather than carrying a trailing memory footprint through the rest of the function execution.
Framing it this way turns the dual-scoping model into a clear choice of engineering intent: omit var when you want rapid prototyping flexibility; use var when you want to intentionally lock down scopes for maximum deterministic efficiency.
There is a counter-intuitive cognitive dissonance around adding “var” in order to reduce variability in the scope. Perhaps local would be a cleaner semantic choice
Ultimately this is a question about what is fixed versus what is variable.
As a programmer I want maximum freedom and convenience to express my design intent. And given that many designs will come from Python code, forcing an incompatible paradigm shift from the start would seem wrong to me.
Instead of “requiring explicit block scoped variables” I would prefer “enabling local block scoped variables” as an optional optimization. It’s just that “var” doesn’t capture that notion well
Mojo’s ASAP destruction policy already guarantees that a value is destroyed after its last use, regardless of the scope. In that sense I do not see how smaller scopes help optimizing.
I do see the point of discomfort with var not being enforced but also the point of Python compatibility and easier scripting.
Maybe a good middle ground for now would be to add a compiler argument --enforce-var (or --warn-on-missing-var) so that people building complex software can spot their potential bugs, and others can keep scripting.
Thanks for your detailed response. I completely agree with this observation:
“There is a counter-intuitive cognitive dissonance around adding “var” in order to reduce variability in the scope. Perhaps local would be a cleaner semantic choice.”
I didn’t understand what was happening in my code because I never expected the scoping behavior to change depending on whether I used var or not. I assumed the scope of a variable would be the same regardless of declaration style, and that implicit declaration was just a convenience for Python compatibility. It took me about an hour to figure out the actual behavior. At first I thought it was a bug, but then I checked the documentation — which led to my first post here.
I’m agnostic about whether block scope is inherently better than function scope. I’m used to function scope (like in Python), but I could probably adapt to block scope too.
I recall JavaScript added proper block scoping (let/const) in ES6, and you can create arbitrary scopes just by wrapping code in {} when needed. Personally, I’ve never felt the need to use that pattern.
In any case, there should be a consistent default for variable scoping, independent of how the variable is declared. The current difference in behavior between implicit and explicit declarations was the main source of confusion for me.
IMO consolidating fn and def into def was a bold and worthwhile decision by Modular. It reduced Mojo’s complexity and made raising functions explicit.
I think consolidating variable declaration syntax and semantics before releasing Mojo 1.0 would be an equally bold and worthwhile decision. This would further reduce Mojo‘s complexity.
Maybe one „soft“ consolidation would be to enforce var declarations in any function that has at least one var (or ref) declaration. So there would be no strange mixing of declaration styles. Functions having explicit var declarations would have the full benefits and guarantees of required explicit var declarations while still allowing for „scripting“ functions without var declarations.
def explicit() -> Int: # OK
var a = 1
var b = 2
return a + b
def script() -> Int # OK
a = 1
b = 2
return a + b
def mixed() -> Int # NOK
var a = 1
b = 2 # error: explicit var declaration required
return a + b
I think this would be minimal invasive and fulfill the need of both programming styles.
But I still would prefer having just explicit var declarations because this would reduce the overall complexity of Mojo.
As someone who * just now * introduced a bug by accidentally invoking implicit variable declarations, I strongly wish there was a way to require only explicit (var) variable declarations.
Since this happened only moments after reading this thread, I felt an urgent need to chime in.
I will personally always use “var” to signify the first declaration of a new variable; I would love tools that ensure I’m doing this correctly (linter, compiler warnings / errors, what have you); and I’d at least like to not lose some obvious way to see where names are first declared. Not having a walrus or ‘var’ for this signal would make me a bit sad. It helps readability.
The strange scoping differences I suppose I do understand - you don’t want to alienate the Python audience and what they’re used to, but most people from systems backgrounds will want something more explicit (often verbose). Thus, I have no strong say for scoping - I trust there’s good intent with the target audience of this language. Good discussion, lots to learn!