I wrote mainpy for this a while back, but I’d also like to see it as part of the language:
mainpy’s approach is like the OP’s, where the decorator calls the decorated function immediately, so it has the same drawback of requiring the main function to be defined last. So this produces a NameError:
from mainpy import main
@main
def app():
hello() # NameError
def hello():
print('hello')
Ideally a @main decorator should make the above code equivalent to:
def app():
hello()
def hello():
print('hello')
if __name__ == '__main__':
app()
Yeah it doesn’t take much to make this feature part of the language since the main program runner (the pymain_run_python function) already produces an exitcode to tell whether the main program completed successfully.
So we just need a @main decorator to set a global dunder __main_func__ to the decorated function, and let the main program runner call the dunder if exitcode is 0.
Here’s an implementation:
Python/bltinmodule.c
static PyObject *
builtin_main(PyObject *self, PyObject *func)
{
if (PyDict_SetItemString(PyEval_GetGlobals(), "__main_func__", func) < 0) {
return NULL;
}
return Py_NewRef(func);
}
Modules/main.c
static void
pymain_run_main_func(int *exitcode)
{
PyObject *main_module = PyImport_AddModuleRef("__main__");
if (main_module == NULL) {
PyErr_Clear();
return;
}
PyObject *globals = PyModule_GetDict(main_module);
Py_DECREF(main_module);
if (globals == NULL) {
PyErr_Clear();
return;
}
PyObject *func;
int rc = PyDict_GetItemStringRef(globals, "__main_func__", &func);
if (rc < 0) {
PyErr_Clear();
}
if (rc <= 0) {
return;
}
PyObject *result = PyObject_CallNoArgs(func);
Py_DECREF(func);
if (result == NULL) {
*exitcode = pymain_err_print(exitcode) ? *exitcode : 1;
PyErr_Clear();
return;
}
Py_DECREF(result);
}
static void
pymain_run_python(int *exitcode)
{
/* ... */
if (*exitcode == 0) {
pymain_run_main_func(exitcode);
}
/*
pymain_repl(config, exitcode);
... */
}
Why do you need a decorator to assign to a name? Why not just def __main_func__(): instead? And if you’re using a magic name, how about use the fact that the module already HAS a special name instead of creating a new piece of magic that only achieves this one thing? What is the advantage?
Because that would then be technically a breaking change.
I don’t understand. If the decorator simply assigns the function to a global name, how is it less of a breaking change to use that decorator than to directly assign to the magic name?
But that still doesn’t answer the question of how any of this is better than the status quo, which works. And it requires explaining how modules have names, which is much simpler than explaining how decorators work. (And if you don’t explain any of it, all options are simply “put this code and it works”, so there’s no real difference between any of them.)
With your approach, existing code can break:
def __main_func__():
print('hello')
The above code outputs nothing before your change, but outputs hello after your change.
With my approach, the existing code will not behave any differently because applying a decorator is opt-in.
What does “global dunder” mean if it is not the name being assigned to?
Ah sorry I see what you mean now. With my approach def __main_func__(): print 'hello' would still get executed by the main program runner because the runner has no knowledge of the decorator.
A decorator is entirely redundant indeed. And a breaking change can still potentially occur no matter what. One can only expect users not to invent their own dunders because dunders are documented to be reserved for future use.
Since the point of the idea is to make Python beginner-friendly, and knowing that many beginners see dunders as cryptic implementation details, I think a better alternative approach is to make a @main decorator that adds a dunder attribute __main_func__ to the function being decorated, and let the main program runner call the first global function with this dunder attribute upon completion of the main program.
This way, at least the user doesn’t need to see any dunder name like:
def __main_func__():
...
but instead just:
@main
def app():
...
It just occurs to me that one way to allow the runner to have definite knowledge of a @main decorator being applied is to add a new field to PyInterpreterState dedicated to pointing to the main function and make the @main decorator set the field so nothing is needlessly exposed.
And if I import other modules that have functions decorated by @main ? Sure the decorator could walk the stack and itself detect if it’s called while executing the “main” module but what have we really accomplished compared to status quo?
The current logic is very easily explained as the concept of “execute this if running as main module” and apart from being unfamiliar, which is not necessarily the same as unintuitive, for people coming from C like languages I would argue is actually the simpler concept.
The main hurdle seems mostly to be the current spelling of it which is arguably on the cryptic side for something that people encounter relatively early due to it’s prevalence.
To help with that I could see a much stronger argument for some built-in that just clearly spells out what it is, __running_as_main_module__ (could be a function and implemented today using frame inspection but it would lead to some unintuitive behavior of used in a function body which is fine in an extern module but feels too hacky for a built-in)
Then nothing will happen, because then __name__ != "__main__"
Yeah for sure it has to be guarded with a __name__ == '__main__' check in the caller’s frame. And like you said, the main point of the idea is to enhance teachability by avoiding dunder names early on.
Again, I’m dubious as to the real improvement here. There are three options:
if __name__ == '__main__': ...
def __main_func__(): ...
@main
def app(): ...
and all of them, if unexplained, are simply “put this text here to make this work”. Why is a decorator less of a problem than a dunder?
And even then, ALL OF THIS is predicated on needing this condition in the first place!! The vast, VAST majority of Python modules do not need to have this condition. You only need to have a check like this if the module is both importable and runnable. By the time a student is learning about this, it should be entirely reasonable to explain that every module has a __name__ and that the one invoked as a script has a special name. Whereas with the decorator and magic name approaches, this becomes magic for every script that uses it, whether it’s necessary or not.
In my experience it’s not as uncommon as you make it out to be.
In my experience people frequently use if __name__ == ‘__main__’ when they don’t need to (myself included).
But that’s an argument for not doing that, not for making it “more intuitive”.
That doesn’t take away from the fact that there are many valid use cases for if __name__ == '__main__'.
I speak for myself when I remember my early days of learning Python and memorizing the if __name__ == '__main__': ... idiom without understanding what it really means and often spelling it incorrectly as if __main__ == '__name__': ... because it simply looked confusing
whereas when I learned decorators, even though I did not yet know how a wrapper truly works, I immediately grasped the idea syntactically because it just looks like a tag.
Yeah, many recommend using the idiom even when it isn’t necessary because it arguably improves readability by visually separating reusable components from executable logic. And having a main() function called by the guard allows the logics to be easily included in test coverage.