94

I've been programming for over 9 years, and according to the advice of my first programming teacher, I always keep my main() function extremely short.

At first I had no idea why. I just obeyed without understanding, much to the delight of my professors.

After gaining experience, I realized that if I designed my code correctly, having a short main() function just sortof happened. Writing modularized code and following the single responsibility principle allowed my code to be designed in "bunches", and main() served as nothing more than a catalyst to get the program running.

Fast forward to a few weeks ago, I was looking at Python's souce code, and I found the main() function:

/* Minimal main program -- everything is loaded from the library */

...

int
main(int argc, char **argv)
{
    ...
    return Py_Main(argc, argv);
}

Yay python. Short main() function == Good code.

Programming teachers were right.

Wanting to look deeper, I took a look at Py_Main. In its entirety, it is defined as follows:

/* Main program */

int
Py_Main(int argc, char **argv)
{
    int c;
    int sts;
    char *command = NULL;
    char *filename = NULL;
    char *module = NULL;
    FILE *fp = stdin;
    char *p;
    int unbuffered = 0;
    int skipfirstline = 0;
    int stdin_is_interactive = 0;
    int help = 0;
    int version = 0;
    int saw_unbuffered_flag = 0;
    PyCompilerFlags cf;

    cf.cf_flags = 0;

    orig_argc = argc;           /* For Py_GetArgcArgv() */
    orig_argv = argv;

#ifdef RISCOS
    Py_RISCOSWimpFlag = 0;
#endif

    PySys_ResetWarnOptions();

    while ((c = _PyOS_GetOpt(argc, argv, PROGRAM_OPTS)) != EOF) {
        if (c == 'c') {
            /* -c is the last option; following arguments
               that look like options are left for the
               command to interpret. */
            command = (char *)malloc(strlen(_PyOS_optarg) + 2);
            if (command == NULL)
                Py_FatalError(
                   "not enough memory to copy -c argument");
            strcpy(command, _PyOS_optarg);
            strcat(command, "\n");
            break;
        }

        if (c == 'm') {
            /* -m is the last option; following arguments
               that look like options are left for the
               module to interpret. */
            module = (char *)malloc(strlen(_PyOS_optarg) + 2);
            if (module == NULL)
                Py_FatalError(
                   "not enough memory to copy -m argument");
            strcpy(module, _PyOS_optarg);
            break;
        }

        switch (c) {
        case 'b':
            Py_BytesWarningFlag++;
            break;

        case 'd':
            Py_DebugFlag++;
            break;

        case '3':
            Py_Py3kWarningFlag++;
            if (!Py_DivisionWarningFlag)
                Py_DivisionWarningFlag = 1;
            break;

        case 'Q':
            if (strcmp(_PyOS_optarg, "old") == 0) {
                Py_DivisionWarningFlag = 0;
                break;
            }
            if (strcmp(_PyOS_optarg, "warn") == 0) {
                Py_DivisionWarningFlag = 1;
                break;
            }
            if (strcmp(_PyOS_optarg, "warnall") == 0) {
                Py_DivisionWarningFlag = 2;
                break;
            }
            if (strcmp(_PyOS_optarg, "new") == 0) {
                /* This only affects __main__ */
                cf.cf_flags |= CO_FUTURE_DIVISION;
                /* And this tells the eval loop to treat
                   BINARY_DIVIDE as BINARY_TRUE_DIVIDE */
                _Py_QnewFlag = 1;
                break;
            }
            fprintf(stderr,
                "-Q option should be `-Qold', "
                "`-Qwarn', `-Qwarnall', or `-Qnew' only\n");
            return usage(2, argv[0]);
            /* NOTREACHED */

        case 'i':
            Py_InspectFlag++;
            Py_InteractiveFlag++;
            break;

        /* case 'J': reserved for Jython */

        case 'O':
            Py_OptimizeFlag++;
            break;

        case 'B':
            Py_DontWriteBytecodeFlag++;
            break;

        case 's':
            Py_NoUserSiteDirectory++;
            break;

        case 'S':
            Py_NoSiteFlag++;
            break;

        case 'E':
            Py_IgnoreEnvironmentFlag++;
            break;

        case 't':
            Py_TabcheckFlag++;
            break;

        case 'u':
            unbuffered++;
            saw_unbuffered_flag = 1;
            break;

        case 'v':
            Py_VerboseFlag++;
            break;

#ifdef RISCOS
        case 'w':
            Py_RISCOSWimpFlag = 1;
            break;
#endif

        case 'x':
            skipfirstline = 1;
            break;

        /* case 'X': reserved for implementation-specific arguments */

        case 'U':
            Py_UnicodeFlag++;
            break;
        case 'h':
        case '?':
            help++;
            break;
        case 'V':
            version++;
            break;

        case 'W':
            PySys_AddWarnOption(_PyOS_optarg);
            break;

        /* This space reserved for other options */

        default:
            return usage(2, argv[0]);
            /*NOTREACHED*/

        }
    }

    if (help)
        return usage(0, argv[0]);

    if (version) {
        fprintf(stderr, "Python %s\n", PY_VERSION);
        return 0;
    }

    if (Py_Py3kWarningFlag && !Py_TabcheckFlag)
        /* -3 implies -t (but not -tt) */
        Py_TabcheckFlag = 1;

    if (!Py_InspectFlag &&
        (p = Py_GETENV("PYTHONINSPECT")) && *p != '\0')
        Py_InspectFlag = 1;
    if (!saw_unbuffered_flag &&
        (p = Py_GETENV("PYTHONUNBUFFERED")) && *p != '\0')
        unbuffered = 1;

    if (!Py_NoUserSiteDirectory &&
        (p = Py_GETENV("PYTHONNOUSERSITE")) && *p != '\0')
        Py_NoUserSiteDirectory = 1;

    if ((p = Py_GETENV("PYTHONWARNINGS")) && *p != '\0') {
        char *buf, *warning;

        buf = (char *)malloc(strlen(p) + 1);
        if (buf == NULL)
            Py_FatalError(
               "not enough memory to copy PYTHONWARNINGS");
        strcpy(buf, p);
        for (warning = strtok(buf, ",");
             warning != NULL;
             warning = strtok(NULL, ","))
            PySys_AddWarnOption(warning);
        free(buf);
    }

    if (command == NULL && module == NULL && _PyOS_optind < argc &&
        strcmp(argv[_PyOS_optind], "-") != 0)
    {
#ifdef __VMS
        filename = decc$translate_vms(argv[_PyOS_optind]);
        if (filename == (char *)0 || filename == (char *)-1)
            filename = argv[_PyOS_optind];

#else
        filename = argv[_PyOS_optind];
#endif
    }

    stdin_is_interactive = Py_FdIsInteractive(stdin, (char *)0);

    if (unbuffered) {
#if defined(MS_WINDOWS) || defined(__CYGWIN__)
        _setmode(fileno(stdin), O_BINARY);
        _setmode(fileno(stdout), O_BINARY);
#endif
#ifdef HAVE_SETVBUF
        setvbuf(stdin,  (char *)NULL, _IONBF, BUFSIZ);
        setvbuf(stdout, (char *)NULL, _IONBF, BUFSIZ);
        setvbuf(stderr, (char *)NULL, _IONBF, BUFSIZ);
#else /* !HAVE_SETVBUF */
        setbuf(stdin,  (char *)NULL);
        setbuf(stdout, (char *)NULL);
        setbuf(stderr, (char *)NULL);
#endif /* !HAVE_SETVBUF */
    }
    else if (Py_InteractiveFlag) {
#ifdef MS_WINDOWS
        /* Doesn't have to have line-buffered -- use unbuffered */
        /* Any set[v]buf(stdin, ...) screws up Tkinter :-( */
        setvbuf(stdout, (char *)NULL, _IONBF, BUFSIZ);
#else /* !MS_WINDOWS */
#ifdef HAVE_SETVBUF
        setvbuf(stdin,  (char *)NULL, _IOLBF, BUFSIZ);
        setvbuf(stdout, (char *)NULL, _IOLBF, BUFSIZ);
#endif /* HAVE_SETVBUF */
#endif /* !MS_WINDOWS */
        /* Leave stderr alone - it should be unbuffered anyway. */
    }
#ifdef __VMS
    else {
        setvbuf (stdout, (char *)NULL, _IOLBF, BUFSIZ);
    }
#endif /* __VMS */

#ifdef __APPLE__
    /* On MacOS X, when the Python interpreter is embedded in an
       application bundle, it gets executed by a bootstrapping script
       that does os.execve() with an argv[0] that's different from the
       actual Python executable. This is needed to keep the Finder happy,
       or rather, to work around Apple's overly strict requirements of
       the process name. However, we still need a usable sys.executable,
       so the actual executable path is passed in an environment variable.
       See Lib/plat-mac/bundlebuiler.py for details about the bootstrap
       script. */
    if ((p = Py_GETENV("PYTHONEXECUTABLE")) && *p != '\0')
        Py_SetProgramName(p);
    else
        Py_SetProgramName(argv[0]);
#else
    Py_SetProgramName(argv[0]);
#endif
    Py_Initialize();

    if (Py_VerboseFlag ||
        (command == NULL && filename == NULL && module == NULL && stdin_is_interactive)) {
        fprintf(stderr, "Python %s on %s\n",
            Py_GetVersion(), Py_GetPlatform());
        if (!Py_NoSiteFlag)
            fprintf(stderr, "%s\n", COPYRIGHT);
    }

    if (command != NULL) {
        /* Backup _PyOS_optind and force sys.argv[0] = '-c' */
        _PyOS_optind--;
        argv[_PyOS_optind] = "-c";
    }

    if (module != NULL) {
        /* Backup _PyOS_optind and force sys.argv[0] = '-c'
           so that PySys_SetArgv correctly sets sys.path[0] to ''
           rather than looking for a file called "-m". See
           tracker issue #8202 for details. */
        _PyOS_optind--;
        argv[_PyOS_optind] = "-c";
    }

    PySys_SetArgv(argc-_PyOS_optind, argv+_PyOS_optind);

    if ((Py_InspectFlag || (command == NULL && filename == NULL && module == NULL)) &&
        isatty(fileno(stdin))) {
        PyObject *v;
        v = PyImport_ImportModule("readline");
        if (v == NULL)
            PyErr_Clear();
        else
            Py_DECREF(v);
    }

    if (command) {
        sts = PyRun_SimpleStringFlags(command, &cf) != 0;
        free(command);
    } else if (module) {
        sts = RunModule(module, 1);
        free(module);
    }
    else {

        if (filename == NULL && stdin_is_interactive) {
            Py_InspectFlag = 0; /* do exit on SystemExit */
            RunStartupFile(&cf);
        }
        /* XXX */

        sts = -1;               /* keep track of whether we've already run __main__ */

        if (filename != NULL) {
            sts = RunMainFromImporter(filename);
        }

        if (sts==-1 && filename!=NULL) {
            if ((fp = fopen(filename, "r")) == NULL) {
                fprintf(stderr, "%s: can't open file '%s': [Errno %d] %s\n",
                    argv[0], filename, errno, strerror(errno));

                return 2;
            }
            else if (skipfirstline) {
                int ch;
                /* Push back first newline so line numbers
                   remain the same */
                while ((ch = getc(fp)) != EOF) {
                    if (ch == '\n') {
                        (void)ungetc(ch, fp);
                        break;
                    }
                }
            }
            {
                /* XXX: does this work on Win/Win64? (see posix_fstat) */
                struct stat sb;
                if (fstat(fileno(fp), &sb) == 0 &&
                    S_ISDIR(sb.st_mode)) {
                    fprintf(stderr, "%s: '%s' is a directory, cannot continue\n", argv[0], filename);
                    fclose(fp);
                    return 1;
                }
            }
        }

        if (sts==-1) {
            /* call pending calls like signal handlers (SIGINT) */
            if (Py_MakePendingCalls() == -1) {
                PyErr_Print();
                sts = 1;
            } else {
                sts = PyRun_AnyFileExFlags(
                    fp,
                    filename == NULL ? "<stdin>" : filename,
                    filename != NULL, &cf) != 0;
            }
        }

    }

    /* Check this environment variable at the end, to give programs the
     * opportunity to set it from Python.
     */
    if (!Py_InspectFlag &&
        (p = Py_GETENV("PYTHONINSPECT")) && *p != '\0')
    {
        Py_InspectFlag = 1;
    }

    if (Py_InspectFlag && stdin_is_interactive &&
        (filename != NULL || command != NULL || module != NULL)) {
        Py_InspectFlag = 0;
        /* XXX */
        sts = PyRun_AnyFileFlags(stdin, "<stdin>", &cf) != 0;
    }

    Py_Finalize();
#ifdef RISCOS
    if (Py_RISCOSWimpFlag)
        fprintf(stderr, "\x0cq\x0c"); /* make frontend quit */
#endif

#ifdef __INSURE__
    /* Insure++ is a memory analysis tool that aids in discovering
     * memory leaks and other memory problems.  On Python exit, the
     * interned string dictionary is flagged as being in use at exit
     * (which it is).  Under normal circumstances, this is fine because
     * the memory will be automatically reclaimed by the system.  Under
     * memory debugging, it's a huge source of useless noise, so we
     * trade off slower shutdown for less distraction in the memory
     * reports.  -baw
     */
    _Py_ReleaseInternedStrings();
#endif /* __INSURE__ */

    return sts;
}

Good God Almighty...it is big enough to sink the Titanic.

It seems as though Python did the "Intro to Programming 101" trick and just moved all of main()'s code to a different function called it something very similar to "main".

Here's my question: Is this code terribly written, or are there other reasons to have a short main function?

As it stands right now, I see absolutely no difference between doing this and just moving the code in Py_Main() back into main(). Am I wrong in thinking this?

10
  • 4
    wouldn't that be better for codereviews.stackexchange.com?
    – foobar
    Commented Jun 20, 2011 at 21:24
  • 42
    @Luzhin, no. I'm not asking anyone to review Python's source code. This is programming question.
    – riwalk
    Commented Jun 20, 2011 at 21:25
  • 5
    TBH, half the code is options processing, and anytime your program supports lot's of options, and you write a custom processor, this is what you end up doing...
    – Nim
    Commented Jun 20, 2011 at 21:30
  • 7
    @Star No, Programmers.SE is also for best practices, coding styles, etc. Actually, that's what I visit the site for. Commented Jun 20, 2011 at 21:34
  • 5
    @Nim, I understand that is what it is doing, but there is no reason not to write it as options = ParseOptionFlags(argc,argv) where options is a struct that contains the variables Py_BytesWarningFlag, Py_DebugFlag, etc...
    – riwalk
    Commented Jun 20, 2011 at 21:35

11 Answers 11

137

You cannot export main from a library, but you can export Py_Main, and then anyone using that library can "call" Python many times with different arguments in the same program. At that point, python becomes just another consumer of the library, little more than a wrapper for the library function; it calls Py_Main just like everyone else.

17
  • 26
    I suppose it might be more accurate to say you can't import it, @Shoosh. The C++ standard forbids calling it from your own code. Besides, its linkage is implementation-defined. Also, returning from main effectively calls exit, which you usually don't want a library to do. Commented Jun 20, 2011 at 21:53
  • 3
    @Coder, see C++03 §3.6.1/5: "A return statement in main has the effect of leaving the main function … and calling exit with the return value as the argument." Also see §18.3/8, which explains that "objects with static storage duration are destroyed" and "all open C streams … are flushed" when you call exit. C99 has similar language. Commented Jun 21, 2011 at 13:58
  • 1
    @Coder, whether exit leaves main is irrelevant. We're not discussing the behavior of exit. We're discussing the behavior of main. And the behavior of main includes the behavior of exit, whatever that may be. That's what makes it undesirable to import and call main (if doing such a thing is even possible or allowed). Commented Jun 21, 2011 at 15:22
  • 3
    @Coder, if returning from main does not have the effect of calling exit on your compiler, then your compiler doesn't follow the standard. That the standard dictates such behavior for main proves that there is something special about it. The special thing about main is that returning from it has the effect of calling exit. (How it does that is up to compiler writers. The compiler could simply insert code in the function epilogue that destroys static objects, calls atexit routines, flushes files, and terminates the program — which, again, isn't something you want in a library.) Commented Jun 21, 2011 at 15:53
  • 1
    C is a little different from C++ in that regard, @Martin. C allows recursive calls to main, so it says that returning from the initial call to main is the same as calling exit. In the scenario we're imagining here, there are two main functions: the host application's, and the one you import from a library. Calling the imported function would be the initial call of that function, so returning from it should terminate your program. (I doubt how many implementations actually work that way, though, since they probably don't expect to export main anyway.) Commented Jun 21, 2011 at 21:07
44

It's not that main shouldn't be long so much as you should avoid any function being too long. main is just a special case of function. Longer functions get very hard to grok, decrease maintainability, and are generally tougher to work with. By keeping functions (and main) shorter you generally improve the quality of your code.

In your example there is no benefit at all to moving the code out of main.

5
  • 9
    The golden word may be "reuse". A long main isn't very reusable.
    – S.Lott
    Commented Jun 20, 2011 at 22:11
  • 1
    @S - That's one golden word. Another is OMG!!! ADHD JUST KICKED IN!!!! or in layman terms: legibility. Commented Jun 20, 2011 at 22:18
  • 4
    main() also has some restrictions that other functions do not have. Commented Jun 20, 2011 at 23:33
  • 1
    Also main() has no real meaning. Your code should all mean something to another programmer. I use main to parse arguments and that's it--and I even delegate that if it's more than a few lines.
    – Bill K
    Commented Jun 21, 2011 at 22:07
  • @Bill K: Good point, using main() only to parse arguments (and starting the rest of the program) also conforms to the single-responsibility principle.
    – Giorgio
    Commented Mar 17, 2013 at 11:41
29

One reason to make main() short involves unit testing. main() is the one function that cannot be unit tested, so it makes sense to extract the majority of the behavior into another class that can be unit tested. This goes along with what you said

Writing modularized code and following the single responsibility principle allowed my code to be designed in "bunches", and main() served as nothing more than a catalyst to get the program running.

Note: I got the idea from here.

1
  • Another good one. Never thought of that aspect.
    – riwalk
    Commented Jun 20, 2011 at 22:46
15

It's rarely a good idea for main to be long; as with any function (or method) if it's long you're probably missing opportunities for refactoring.

In the specific case you mention above, main is short because all that complexity is factored out into Py_Main; if you want your code to behave like a python shell, you can just use that code without a lot of fiddling around. (It has to be factored like that because it doesn't work well if you put main in a library; odd things happen if you do.)

EDIT:
To clarify, main can't be in a static library because it has no explicit link to it and so won't be linked in correctly (unless you colocate it in an object file with something that is referred to, which is just horrible!) Shared libraries are usually treated as being similar (again, to prevent confusion) though on a lot of platforms an additional factor is that a shared library is just an executable without a bootstrap section (of which main is just the last and most visible part).

1
  • 1
    In short, don't put main in a library. It either won't work or it will confuse you terribly. But delegating virtually all its work to a function that is in a lib, that's often sensible. Commented Jun 20, 2011 at 21:53
7

Main should be short for the same reason that any function should be short. The human brain has a hard time keeping large amounts of unpartitioned data in memory at once. Break it up into logical chunks so that it is easy for other developers (as well as yourself!) to digest and reason about.

And yes, your example is terrible and hard to read, let alone maintain.

5
  • Yes I always suspected that the code itself was terrible (though the question dealt with the placement of the code, not the code itself). I'm afraid that my vision of Python has been inherently damaged as a result...
    – riwalk
    Commented Jun 20, 2011 at 22:58
  • 1
    @stargazer: I don't know that the code itself is terrible, just that it is not organized well for human consumption. That said, there is plenty of "ugly" code out there that works well and performs great. Code beauty isn't everything, though we should always try our best to write the cleanest code possible. Commented Jun 20, 2011 at 23:07
  • meh. To me, they are one and the same. Clean code tends to be more stable.
    – riwalk
    Commented Jun 21, 2011 at 2:46
  • The code is not terrible, mainly there are switch cases and the handling of multiple platforms. What exactly you find terrible?
    – Francesco
    Commented Jun 21, 2011 at 5:27
  • @Francesco: Sorry, "Terrible" from a maintenance and readability perspective, not a functional one. Commented Sep 6, 2011 at 21:06
2

Its best practice to keep ALL of your functions short, not just main. However "short" is subjective, it depends on the size of your program and the language you are using.

1

Some people enjoy 50+ functions that do nothing else, but wrap a call to another function. I would rather prefer normal main function that does the main program logic. Well structured of course.

int main()
{
CheckInstanceCountAndRegister();
InitGlobals();
ProcessCmdParams();
DoInitialization();
ProgramMainLoopOrSomething();
DeInit();
ClearGlobals();
UnregisterInstance();
return 0; //ToMainCRTStartup which cleans heap, etc.
}

I don't see any reason why I should wrap anything of that inside a wrapper.

It's purely a personal taste.

1
  • 1
    Because it document's the code. You can write code this way without the need to (almost) ever write comments. And when you change the code, the documentation changes automagically :-). Commented Jun 28, 2011 at 9:49
0

There's no requirement for main to be of any length whatsoever, other than coding standards. main is a function as any other, and as such it's complexity should be below 10 (or whatever your coding standards say). That's it, anything else is rather argumentative.

edit

main shouldn't be short. Or long. It should include the functionality it is required to perform based on your design, and adhere to the coding standards.

As to the specific code in your question - yes, it's ugly.

As to your second question - yes, you are wrong. Moving all that code back into main doesn't allow you using it modulary as a library by linking Py_Main from outside.

Now am I clear?

8
  • I didn't ask whether it can be long. I asked why shouldn't it be long.
    – riwalk
    Commented Jun 20, 2011 at 21:31
  • “Complexity below 10”? Is there a measurement unit for that? Commented Jun 20, 2011 at 21:32
  • @Stargazer712 Function length is usually regulated by the coding standards as well. It's a readability issue (and complexity, usually long functions are branched so that the complexity is way above 20), and as I said - main is no different than any other function in this regard.
    – littleadv
    Commented Jun 20, 2011 at 21:33
  • @Donal - yes, click on the link.
    – littleadv
    Commented Jun 20, 2011 at 21:33
  • I'm going to have to downvote this one bud. You're completely missing the intent of the question.
    – riwalk
    Commented Jun 20, 2011 at 21:38
0

Here's a new pragmatic reason too keep main short from the GCC 4.6.1 Changelog:

On most targets with named section support, functions used only at startup (static constructors and main), functions used only at exit and functions detected to be cold are placed into separate text segment subsections. This extends the -freorder-functions feature and is controlled by the same switch. The goal is to improve the startup time of large C++ programs.

Highlighting added by me.

0

Do not assume that just because a bit of software is good all of the code behind that software is good. Good software and good code are not the same thing and even where good software is backed by good code, it is inevitable that in a large project there will be places where standards slip.

It is good practice to have a shortmainfunction, but that is really just a special case of the general rule that it is better to have short functions. Short functions are easier to understand and easier to debug as well as being better at sticking to the kind of 'single purpose' design that makes programs more expressive. main is, perhaps, a more important place to stick to the rule since anyone who wants to understand the program must understand main while more obscure corners of the codebase may be visited less often.

But, the Python codebase isn't pushing the code out toPy_Mainin order to game this rule but because you cannot exportmainfrom a library nor call it as a function.

-1

There are several technical answers above, lets leave that aside.

A main should be short because it should be a bootstrap. The main should instantiate a small number of objects, often one, which do the work. Like anywhere else, those objects should be well designed, cohesive, loosely coupled, encapsulated, ...

While there might be technical reasons to have a one-line main call another monster method, in principle you are correct. From a software engineering perspective, nothing has been gained. If the choice is between a one line main calling a monster method, and main itself being a monster method, the latter is fractionally less bad.

1
  • You're assuming that "C++ code should use objects and only objects". That's not true, C++ is a multiparadigm language, and doesn't force everything into an OO mold like some other languages.
    – Ben Voigt
    Commented Jun 21, 2011 at 4:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.