What Was Your Biggest “Wait, What?” Moment in Mojo?

Hi everyone. I’m Erica from the Mojo Docs team :waving_hand:

We’re trying to get a better picture of where people struggle when adopting Mojo, especially early on. I’m not looking for “what’s wrong with Mojo” but for what felt painful, confusing, frustrating, or slow to click for you. This can be about concepts, naming, the runtime vs. compile-time divide, or anything else, as long as it’s a specific feature or pattern that caused friction during adoption.

If you’re willing, I’d really appreciate hearing about any of the following. Answer as many or as few as you like. Even one sentence is helpful.

  • What was the hardest or most painful thing to wrap your head around, early in your Mojo journey?
  • What took longer than you expected to understand, and would have benefited from clearer or more complete examples or coverage?
  • What was something you misunderstood initially, or had to unlearn from previous languages? For example, was anything conceptually out-of-line with industry language standards that threw you?
  • What was a concept or design pattern that only made sense after you had already struggled with it?
  • What made you think, “I wish the docs had told me this earlier”? Or, “Why does this come before that in the docs?”

Your feedback helps us smooth Mojo adoption. It gives direction on what needs clearer or earlier explanation in the docs, where examples or mental models are missing, and and where it may be worth sharing feedback with the language team about patterns or practices that create confusion.

No need to be polished or exhaustive. Rough thoughts are perfect. What made you want to throw your hands up or walk away?

Thanks for helping make Mojo easier to learn.

– Erica

10 Likes

mojopkg files. They are in the docs but there are 0 valid use cases for them on the user side. To this day I still don’t understand why it’s still in the docs and why it’s not treated as an implementation detail. Either it should be in the docs and clearly state in which case they must be used, either they should be removed from the docs. There are been a few discussions with the compiler team about it, and every time the answer was “It’s an implementation detail but we expose it just in case it’s useful”

Some background: Version compatibility for .mojopkg files - #3 by denis

3 Likes

I suppose for me it is/was not so much one thing or another, but rather a bit of everything. I’ll admit, I started learning expecting it to be quite similar to Python, but as Owen has pointed out, it’s closer to C++/Rust/systems programming languages but “wears a snakeskin jacket”. So, perhaps for newcomers it’s better to approach it as a new language rather than a “superset of Python”. There are a number of (growing) discrepancies that you need to know about via docs/examples/etc. These range from fairly obvious to more obscure if you don’t keep up with things (ex: List not being able to have mixed types, setting a List to a new variable always performing a deep copy, upcoming changes to Int making division return Int instead of Float). Beyond the slight discrepancies with Python, newcomers from Python really need to get a grasp of a whole slew of new programming paradigms just to be able to code up more than a few lines:

  • The strict type system and generics
  • Traits and how they interact with the strict type system
  • Origins/lifetimes and how they interact with the strict type system
  • comptime values and parameters, and how they interact with and enhance/limit runtime calculations

This is before getting into features that either are very new or not yet fully implemented such as linear types, how concurrency is handled, etc. The Game of Life intro was nice, but didn’t really introduce any of these concepts, at least not at the time I went through it.

And as far as intros, I’m probably the odd one out on this one, but I didn’t find the GPU programming tutorial as helpful as others made it seem, certainly not the first 10 or so that felt like “can you use 2D numpy indexes and understand what the corresponding index would be if you called .flatten().” I was hoping for more emphasis on/explanation of other fundamental Mojo/Max things like “what’s this whole stack_allocation thing”, “why is there dtype, DType, and then Scalar[dtype] and what’s the difference”, “what if I want some of these comptime values to be runtime instead”, “why are we using UnsafePointer here and not Pointer”, "why are we using MutAnyOrigin, “why can’t we call kernels within other kernels like normal functions”, etc. There are a couple of custom ops examples near the end for using pytorch tensors that are close, but I really was expecting to see a “here’s how you put your numpy array through a GPU kernel”, “here’s the ‘equivalent’ array container/data structure that it’s converted to in Mojo”, and “here’s how to manipulate that data structure”.

Finally, on the “playing around” and debugging side of things, this is where there I probably had the most frustration. I use/used the Mojo extension for VS Code which seems to constantly have issues (though has greatly improved) with crashing and/or giving completely wrong errors. Many of the errors that I come across are relatively vague, cryptic, or confusing for new learners. It takes a bit to get used to Mojo’s ASAP destruction, so I tried to opt for printing out values, but the majority of which don’t conform to the Writable trait so it’s been incredibly frustrating trying to just prove to myself that the things were what I thought they were, see what methods/attributes are available to a struct (without just opening a separate window to the API docs), etc. This was a bit of a shock coming from Python where you can print almost everything as a “poor man’s debugger”. This combined with the lack of a native plotting/graphing library has made “sandbox learning” quite difficult.

5 Likes

Overall, I think the docs are pretty dang solid, especially for a language that is pre-1.0. I’ve rarely had to dip into the std source code which is great (unlike Zig, where I basically live with it open). Most examples are good when they’re included. Of course, more examples for methods etc. I don’t think ever hurts.

  1. Having to use rebind[]() a lot and not always getting why I need to (usually getting elements or slices out of LayoutTensors). At least the compiler helped me ‘solve’ those, but documentation on that could possibly use a small expansion for now. It seems a powerful tool when there might be safer patterns.

  2. Origins, closure syntax (unified, capturing keywords) I think have been promised better documentation already (and eventually I think some of those words might not be needed?). I would love more help understanding origins with examples etc.

I still need to explore how much can actually be done at comptime, the lines aren’t super clear to me at this time.

  1. I probably would’ve liked to see more coverage of where things break from Python and why those tradeoffs are worth it. This is a good segue into my last thoughts…

I’d like to echo @Serotonin about the “snakeskin” jacket. As a “yes, and”, I wish there was as much effort put into reaching Python devs as there was reaching people who write C++, Zig, Rust, etc.

The tension between wanting to be Pythonic and also be a systems language (and GPU platform etc.) has some silly moments; perhaps there’s a role for documentation to help bridge these worlds in the meantime and market Mojo not as a language of compromises but its own unique, soulful project.

Last couple notes: I haven’t checked the GPU tutorial in a while, but I’d be interested to hear other responses on that topic. And ”What made you want to throw your hands up or walk away?” Only my own skill issues! Cheers!

2 Likes

Hi, thanks for asking the questions.
It is really great to see this approach (refinement cycles process).
It will get better and better with each iteration.

To me it all started to become really easy when:
Learned that an value need destructor and init.
And it all came down to learn that values are in an Pointer (UnsafePointer).
It is also related as to why we need __moveinit__ :smiley:
And also related to why we need Origin.
It also explains why we need emplacing values ? (No need to call __del__)
And explained also why we need types (to call the proper __del__ :recycling_symbol: )

I always was confused with the concept of pointer in C so never “learned” it,
but in mojo, it is very simple because address is an field.
(i learned pointers trough mojo :+1: )

I noticed that most of us learned it all because in the beginning there was no List lol.
The only thing available was an allocatable pointer (no origin), expressive dunders and parameters.
There was no traits neither, and people quickly realized that it was needed to make it generic.

It seem like creating an List from scratch could be an “lets learn it all” path.
(suggestion for tutorial)
I think it has the potential to teach all concepts progressively.

1 Like

Self referenceing structs
Not being able to use safer pointers in self referential structs is a big “wait what?” issue I had:

from memory import ArcPointer

@fieldwise_init
struct A(Movable, Writable):
    var a: Optional[ArcPointer[Self]]

fn main():
    var a = A(a=None)
    print(a)
feedback.mojo:5:8: error: struct has recursive reference to itself
struct A(Movable, Writable):

I feel like this is a very quick thing a user from python is going to run into. The solution right now is to use UnsafePointer which isn’t the best permanent solution. I shouldn’t need to use unsafe APIs to do something common / basic like structs that may self reference.

vscode extension
Mojo’s extension needs to support Mojo › Lsp: Include Dirs in the Workspace tab. Currently I can only set these for the global user, which isn’t great when I have multiple projects, and Remote tab which gets wiped whenever I restart my dev container.

3 Likes

See [Feature Request] Metadata in .mojopkg · Issue #5860 · modular/modular · GitHub, we actually have a lot of uses for them.

1 Like

That’s great! Let’s update the docs, so that it’s less confusing for newcomers :slight_smile:

1 Like

I think that needs to happen after the metadata field gets added and we figure out what shape that is.

1 Like

Honestly, after using Mojo for this long, I’ve had so many “wait… what?” moments. But most of them got resolved over time as Mojo kept improving — a lot of the “why doesn’t this exist?!” or “why does it behave like this?!” things eventually got fixed, and the reaction became “yes! that’s how it should work.”

So overall I’m really happy with Mojo and genuinely excited about where it’s headed. Just gotta wait and hope! :grinning_face_with_smiling_eyes:

But if I had to pick one moment? Probably about a year ago after some update, I was debugging something for way too long and couldn’t figure out what was going on. Eventually I traced it down to this:

fn main() raises:
    a = [1, 2, 3, 4]
    print(a[8])

This just… returns 0. No error. No crash. No bounds check — unless you explicitly compile with -D ASSERT=all. For a type like List that feels like it should be “safe”, silently returning garbage instead of yelling at you is a pretty jarring surprise.

5 Likes

I went back through my posts to the discord help bot, and so many of them have been updated in the docs already! It’s time for me to go reread the docs!

The only big missing thing I still see is a section on Iterators. It’s something people will try to create right away, and there’s no stdlib docs covering something like how to implement an iterator.

2 Likes

One more thing - packaging. I think the manual should cover:

  • How to create a “library” project that can be depended on by other projects
  • How to create a “binary” project that can be installed on a system
  • How to get the LSP to “see” a local project you are depending on

Figuring those out took a lot of extra reading and looking at the already published stuff on modular-community the first time around.

3 Likes

I agree this is a problem, and probably worth discussing breaking out into a separate thread.

2 Likes

I might add one more thing to this list: How to distribute a binary (or binary precursor) to run at max speed on unknown end-user hardware, including accelerators.

We’ve been struggling a lot with this one and have come up with a solution, but it’s pretty convoluted. It would be nice to see what the official vision for this is.

2 Likes
  1. The first thing that required some rethinking was the switch from seing variables (always) as references (as in Python) to seing them as chunks of memory that are copied around.

I think the example below violates my assumptions coming from python. At the same time, it may be a good motivating example, why the Mojo behaviour enforcing unique ownership makes sense in the first place.

a = [1, 2, 3]
b = a
b[1] = 4
print(a[1] == b[1]) # False
  1. The second thing (already mentioned by @Serotonin) I struggled with was to understand what DType and its members are vs. the actual data types. In general, what is DType? Is it a struct? Is it a global variable? Is it a module? (I think these things have become clearer through times).

  2. The thing that I am still missing is a proper introduction of Origins. Again: what is an origin actually? Is it a type as others? What happens in an origin_cast? When is that safe to do and when not? The current docs give a decent first (shallow) introduction of the general motivation, but to really understand something, I need to know what will happen if I play around with it.

4 Likes

My frustrations currently is on limited support for LoRA adapters in Max to llama 3 models only I’ll accept any future extensions for Qwen VL or Gemma 3.

Note: Im not pointing out that modular is limited I’m just excluding my interests I have so much to explain on this but it’s awesome to view any contributions.

1 Like

Use MAX, jit compile the hot loop.

1 Like

This isn’t so much a problem with the language itself but very much an issue with language adoption. The LSP is, to put it bluntly, borderline unusable. It’s slow, crashes almost constantly, and will sometimes cause bizarre and buggy syntax highlighting. Restarting it will sometimes fix it but other times will immediately crash again.

I know the language well enough at this point to get by but It’s hard for me to recommend it while this is an issue. I’ve seen this mentioned once or twice by Modular folks so I haven’t made a thread or issue about it but it’s very high on my list of pain points. Tooling is super important for adoption so I really hope to see this mostly ironed out by 1.0.

8 Likes

One thing that was confusing for me coming from Python was the “Writable” trait.

In Python if you have created a class that only has a __repr__ and you use the class directly in print(…) you will get back the __repr__ output. If you also have __str__ implemented and use the object directly in print(…) it will rather use the __str__ output.

I was expecting the same kind of behaviour for Mojo, but it is different.

In Mojo from what I have now seen there is 3 ways you can do this.

@fieldwise_init
struct Dog(Copyable, Stringable, Representable, Writable):
    var name: String
    var age: Int

    # Allows the type to be written into any `Writer`. Example: using print(...)
    fn write_to(self, mut writer: Some[Writer]):
        writer.write("Dog(", self.name, ", ", self.age, ")")

    fn __str__(self) -> String:
        # could also use String.write(self) instead to use write_to if you don't
        # want a differnt string
        return String(self.name, "the dog is", self.age, "years old.", sep=" ")

    # Alternative full representation when calling `repr`
    fn __repr__(self) -> String:
        return String("Dog(name=", repr(self.name), ", age=", repr(self.age), ")")


fn main():
    dog = Dog("Rex", 5)
    print(repr(dog))   # Uses __repr__ | trait: Representable
    print(String(dog)) # Uses __str__  | trait: Stringable
    print(dog)         # Uses write_to | trait: Writable

This is however very different from Python and I don’t see the docs in the trait section mentioning this. Not sure if I missed it being mentioned elsewhere.

The example also wasn’t 100% clear on this and I think having an example like this could be better for people coming from python.

3 Likes