The first thing absolute beginners do when learning Python is printing “Hello, world” to the console. While this might seem like a simple process, there’s a lot more going on than you think. Like did you know the print function is in fact written in the C language and not Python itself?

First up, print is a built-in function. and like all built in functions, it’s comes with your Python interpreter. Like I mentioned before, the implementation for the print function is not written in Python but in the C language. This holds true for all inbuilt functions like input, sum, etc.

Here’s the entire C implementation.

static PyObject *
builtin_print_impl(PyObject *module, PyObject *args, PyObject *sep,
                   PyObject *end, PyObject *file, int flush)
/*[clinic end generated code: output=3cfc0940f5bc237b input=c143c575d24fe665]*/
{
    int i, err;

    if (file == Py_None) {
        PyThreadState *tstate = _PyThreadState_GET();
        file = _PySys_GetAttr(tstate, &_Py_ID(stdout));
        if (file == NULL) {
            PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout");
            return NULL;
        }

        /* sys.stdout may be None when FILE* stdout isn't connected */
        if (file == Py_None) {
            Py_RETURN_NONE;
        }
    }

    if (sep == Py_None) {
        sep = NULL;
    }
    else if (sep && !PyUnicode_Check(sep)) {
        PyErr_Format(PyExc_TypeError,
                     "sep must be None or a string, not %.200s",
                     Py_TYPE(sep)->tp_name);
        return NULL;
    }
    if (end == Py_None) {
        end = NULL;
    }
    else if (end && !PyUnicode_Check(end)) {
        PyErr_Format(PyExc_TypeError,
                     "end must be None or a string, not %.200s",
                     Py_TYPE(end)->tp_name);
        return NULL;
    }

    for (i = 0; i < PyTuple_GET_SIZE(args); i++) {
        if (i > 0) {
            if (sep == NULL) {
                err = PyFile_WriteString(" ", file);
            }
            else {
                err = PyFile_WriteObject(sep, file, Py_PRINT_RAW);
            }
            if (err) {
                return NULL;
            }
        }
        err = PyFile_WriteObject(PyTuple_GET_ITEM(args, i), file, Py_PRINT_RAW);
        if (err) {
            return NULL;
        }
    }

    if (end == NULL) {
        err = PyFile_WriteString("\n", file);
    }
    else {
        err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
    }
    if (err) {
        return NULL;
    }

    if (flush) {
        if (_PyFile_Flush(file) < 0) {
            return NULL;
        }
    }

    Py_RETURN_NONE;
}

Sorry for that long spam :) Link to the original repository

So, let’s break down this supposedly scary C code. I’ve annotated the code to highlight the important parts.

Part 1

if (file == Py_None) { // Set file to stdout if it has not explicitly been set.
        PyThreadState *tstate = _PyThreadState_GET();
        file = _PySys_GetAttr(tstate, &_Py_ID(stdout));
        if (file == NULL) {
            PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout");
            return NULL;
        }

        if (file == Py_None) {
            Py_RETURN_NONE;
        }
    }

A common misconception is that the print function can only be used to print to the terminal but in fact it can be used to write to any file object. If the optional parameter file is left empty, it prints to the standard output stream or stdout, which if you haven’t guessed already, is the terminal.

Part 2

    if (sep == Py_None) {
        sep = NULL;
    }
    else if (sep && !PyUnicode_Check(sep)) {
        PyErr_Format(PyExc_TypeError,
                     "sep must be None or a string, not %.200s",
                     Py_TYPE(sep)->tp_name);
        return NULL;
    }

Next up is the sep optional parameter, this defines what separates the different arguments passed to the print function. By default, it’s value is a Unicode space.

    if (end == Py_None) {
        end = NULL;
    }
    else if (end && !PyUnicode_Check(end)) {
        PyErr_Format(PyExc_TypeError,
                     "end must be None or a string, not %.200s",
                     Py_TYPE(end)->tp_name);
        return NULL;
    }

You might have noticed that when you print something, a new line is automatically added. This behavior can be controlled by the end optional parameter. By default, this is a new line character \n

Part 3

for (i = 0; i < PyTuple_GET_SIZE(args); i++) {
        if (i > 0) {
            if (sep == NULL) {
                err = PyFile_WriteString(" ", file);
            }
            else {
                err = PyFile_WriteObject(sep, file, Py_PRINT_RAW);
            }
            if (err) {
                return NULL;
            }
        }
        err = PyFile_WriteObject(PyTuple_GET_ITEM(args, i), file, Py_PRINT_RAW);
        if (err) {
            return NULL;
        }
    }

    if (end == NULL) {
        err = PyFile_WriteString("\n", file);
    }
    else {
        err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
    }
    if (err) {
        return NULL;
    }

Next up, the arguments are printed to the file object separated by the sep value and theend value is printed at last.

Part 4

 if (flush) {
        if (_PyFile_Flush(file) < 0) {
            return NULL;
        }
}

Finally, the buffer is flushed if flush is true. By default, the print function buffers the output, meaning that it may not be immediately displayed on the screen or written to a file. However, you can force the output to be flushed immediately by setting the flush parameter to True.

The purpose of buffering is mainly being more efficient on I/O calls. Printing each character one by one leads to many expensive I/O calls instead printing every 3 characters or line by line signifcantly reduces the number of I/O calls.

Thanks for reading!

© 2024 Shiv. All rights reserved.

Decoded: printing in Python

Last updated on