Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Long Hello World (with notes)

The Long Hello World (with notes)

Hello World is often used as a stand-in for "the simplest program", great for teaching and getting people started on their coding journey. But what _really_ happens in that one line? This talk will be a deep dive into the Python interpreter, C libraries, Linux kernel, and beyond. We will poke holes in every abstraction and learn about our computers like never before, both to aid in debugging and to pick the best level of tooling for future development projects.

Avatar for Noah Kantrowitz

Noah Kantrowitz

October 18, 2025
Tweet

More Decks by Noah Kantrowitz

Other Decks in Technology

Transcript

  1. Noah Kantrowitz • He/him • coderanger.net | cloudisland.nz/@coderanger • Kubernetes

    and Python • SRE/Platform for Geomagical Labs, part of IKEA • We do CV/AR for the home PyBay 2025 – Noah Kantrowitz – @[email protected] 2 Hi there, I'm Noah Kantrowitz. I'm an SRE at Geomagical Labs. We do computer vision and augmented reality stuff for IKEA. But I'm not here to talk about that.
  2. print("Hello world") PyBay 2025 – Noah Kantrowitz – @[email protected] 3

    Hello world is a starting point. For most discussions, it's a gentle introduction to making Python do anything at all. From Hello World we could build towards talking about functions, and strings, and all of programming. But we're using it as a starting point for something else.
  3. print("Hello world") CPython Libc Kernel PyBay 2025 – Noah Kantrowitz

    – @[email protected] 4 Rather than looking outward at programming in general, let's look inward, down the stack at the moving pieces involved in this innocuous line of code.
  4. The Players • CPython • Linux • Glibc • x86_64

    PyBay 2025 – Noah Kantrowitz – @[email protected] 5 To set the stage a bit, we're going to look at the specific case of Linux and Glibc. Python runs on many other platforms and combinations, but we want to look at specifics so we need to pick one and this is the most common. All code samples I'll show will be accurate as of writing in September 2025.
  5. The Setup $ python Python 3.14.0 (main) [Clang 17.0.0] on

    darwin Type "help", "credits" or "license" for more information. >>> print("Hello world") PyBay 2025 – Noah Kantrowitz – @[email protected] 6 And our point of departure. We're in a Python interactive shell, we typed in print hello world, and have just pressed enter. What happens next? Not just saying it prints hello world. What really happens next.
  6. Lib/code.py - InteractiveConsole def interact(self, banner=None, exitmsg=None): # ... while

    True: line = input(prompt) more = self.push(line) # ... PyBay 2025 – Noah Kantrowitz – @[email protected] 7 This is our first stop, slightly simplified. At heart the interactive shell is a while loop calling input and then doing something with string it recieves. So we'll have the variable "line" containing our print hello world, then what?
  7. Lib/_pyrepl/console.py - InteractiveColoredConsole def runcode(self, code): try: exec(code, self.locals) except

    SystemExit: raise except BaseException: self.showtraceback() return self.STATEMENT_FAILED return None PyBay 2025 – Noah Kantrowitz – @[email protected] 8 Every one of these layers involves a lot of a calls so we're going to have to skip around a bit or it would take hours. Following down the call stack from that push method, this is what we care about. We have our code from the input and we are calling the exec built-in on it. This is going to parse and execute our line of code. So of course our next question is how does that work?
  8. Lexing and Parsing • Bytes - 0×31 • Decoding •

    Characters - '1' • Tokenizing / Lexing • Tokens - NUMBER "1" • Parsing • Syntax - Constant(value=1) • Compiling • Opcodes - LOAD_CONST 0 (1) PyBay 2025 – Noah Kantrowitz – @[email protected] 9 It's theoretically possible to build a language which executes a program one byte at a time. But this makes any kind of complex syntax much harder so instead all major language go through a number of steps, each giving us a bit more structure. Decoding bytes to unicode characters happens first but that's been covered many times in many places, we're more interested in the other two. Lexing or tokenizing is the step which goes from a string of characters to a list of tokens, and then parsing turns a list of tokens into a syntax tree.
  9. • Python/bltinmodule.c - builtin_exec_impl • Python/pythonrun.c - _PyRun_StringFlagsWithName • Parser/peg_api.c

    - _PyParser_ASTFromString • Parser/pegen.c - _PyPegen_run_parser_from_string • Parser/tokenizer/string_tokenizer.c - _PyTokenizer_FromString • Parser/lexer/lexer.c - tok_get_normal_mode if (Py_ISDIGIT(c)) { if (c == '0') { /* Hex, octal or binary -- maybe. */ c = tok_nextc(tok); if (c == 'x' || c == 'X') { // ... elided return MAKE_TOKEN(NUMBER); PyBay 2025 – Noah Kantrowitz – @[email protected] 10 To actually reach the lexer we need to jump a ways down the stack. In actual usage the lexer is like a Python generator, it makes tokens one at a time as the parser asks for them. But "tok get normal mode" is the bulk of the logic. This function goes through the characters and basically groups them together into tokens along with labeling which type of token it is.
  10. Token Types • NAME, NUMBER, STRING • LPAR, RPAR, PLUS,

    COLON • COMMENT, NEWLINE • INDENT, DEDENT • import token PyBay 2025 – Noah Kantrowitz – @[email protected] 11 The token module in the standard library exposes all the available token types so we can see a few things in here. the main things are names which are anything that looks like an identifier, numbers and strings which are literal values, and syntactically significant punctuation like parentheses, operators, and colons. The lexer also provides indent and dedent tokens so the parser doesn't have to understand tabs versus spaces. This is also where things like line continuations are expanded, again so deeper layers don't have to know about them.
  11. Our Tokens $ echo 'print("Hello world")' | python -m tokenize

    -e 1,0-1,5: NAME 'print' 1,5-1,6: LPAR '(' 1,6-1,19: STRING '"Hello world"' 1,19-1,20: RPAR ')' 1,20-1,21: NEWLINE '\n' 2,0-2,0: ENDMARKER '' PyBay 2025 – Noah Kantrowitz – @[email protected] 12 The tokenize module lets us experiment with the lexer and see the resulting token list. So let's feed it our line of code. We end up with 6 tokens, a name token matching the 5 characters p r i n and t. Then a left paren token, a string literal, a right paren, a newline, and an end of input token. This is more structured than our original string, but still not ready to be run as code, parsing is still to go.
  12. PyBay 2025 – Noah Kantrowitz – @[email protected] 13 The representation

    so far been linear, but when we think about complex code it's not flat. The body of a function is "inside" that definition, or the parameters for a function are "inside" the call. An abstract syntax tree extends this idea, as the parser runs it builds a nested tree structure that encodes all the syntax used.
  13. Grammar/python.gram t_primary[expr_ty]: | a=t_primary '.' b=NAME { _PyAST_Attribute(a, b->v.Name.id) }

    | a=t_primary '[' b=slices ']' { _PyAST_Subscript(a, b) } | a=t_primary '(' b=[arguments] ')' { _PyAST_Call(a, (b) ? ((expr_ty) b)->v.Call.args : NULL, (b) ? ((expr_ty) b)->v.Call.keywords : NULL, ) } | a=atom { a } PyBay 2025 – Noah Kantrowitz – @[email protected] 14 Python's parser is built around a Parsing Expression Grammar or PEG input. This file defines the syntax of Python in terms of the tokens we saw before. Parsing is an incredibly dense topic itself, but the short version is each grammar rule matches some set of tokens in the right order and produces a syntax tree node, which are then combined together into the final AST for the code.
  14. $ echo -n 'print("Hello world")' | python -m ast Module(

    body=[ Expr( value=Call( func=Name(id='print', ctx=Load()), args=[ Constant(value='Hello world')]))]) PyBay 2025 – Noah Kantrowitz – @[email protected] 15 As with the tokenizer before, the Python parser has a command-line module you can play with. Here we can see a module consisting of a single expression, which is a function call with a name of print and one argument of the string hello world. This is now fully structured and ready to be passed forward on its journey towards running.
  15. Interpreted Language Compiled Language • Back to _PyRun_StringFlagsWithName mod =

    _PyParser_ASTFromString(str, name, start, flags, arena); if (mod != NULL) { ret = run_mod(mod, name, globals, locals, flags, arena, source, generate_new_source); } PyBay 2025 – Noah Kantrowitz – @[email protected] 16 Many words have been said over the years about how Python is or isn't an interpreted language and honestly I've mostly lost interest over the debate, but we can show very clearly here that Python is a compiled language because our next stop is the Python compiler.
  16. Virtual Machine • An abstract stack • PUSH 1 •

    PUSH 2 • ADD • POP • Call frame metadata • Thread state PyBay 2025 – Noah Kantrowitz – @[email protected] 17 Before we can talk about compiling Python, we need to look at what that compiler is for. When we compile C code, it's creating CPU instructions for the physical CPU in your computer, but CPython has its own virtual machine. This isn't like a virtual machine running Windows, it's more like a simplified fake machine. The core of it is the stack. If you've worked with Forth or an RPN calculator before this might look familiar, in a stack-based system you can push things onto the stack, operate on them, and then pop a result. The CPython virtual machine also tracks some secondary data like call frame information and thread state.
  17. Opcodes • IMPORT_NAME – Import a module and push it

    • DICT_UPDATE – Pop two dicts and push a single merged dict • LOAD_CONST – Push a constant value • JUMP_BACKWARD – Move the execution pointer backwards • RETURN_GENERATOR – Create a generator object for this function and push it • NOP – Do nothing, be chill PyBay 2025 – Noah Kantrowitz – @[email protected] 18 Because our virtual machine can be a lot more complex than a physical CPU, our low-level instructions can be very specific to Python's patterns. They are called opcodes and are the building blocks for all of Python. The dis module documentation lists all of them and here's a few to set the tone, but new ones get added over time to support new language features and unused ones can be removed. There's usually about 100 at any given time.
  18. • Python/pythonrun.c - run_mod • Python/compile.c - _PyAST_Compile • Python/compile.c

    - compiler_codegen • Python/codegen.c - _PyCodegen_Module • Python/codegen.c - codegen_body for (Py_ssize_t i = first_instr; i < asdl_seq_LEN(stmts); i++) { VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i)); } PyBay 2025 – Noah Kantrowitz – @[email protected] 19 So we're back to "run mod", calling down into the compiler. The core of this is a visitor pattern, with a callback for each type of node in the syntax tree. As it walks down the tree, it generates opcodes and adds them to a code object.
  19. $ echo 'print("Hello world")' | python -m dis 0 RESUME

    0 1 LOAD_NAME 0 (print) PUSH_NULL LOAD_CONST 0 ('Hello world') CALL 1 POP_TOP LOAD_CONST 1 (None) RETURN_VALUE PyBay 2025 – Noah Kantrowitz – @[email protected] 20 Again, the compiler could be a whole talk, but let's just look at our bytecode. As before, the dis module is a CLI tool to display the opcodes for a given input. So our code turns into loading the print function onto the stack,then pushing a NULL because print doens't take a self argument, then the hello world string literal, then run the function call, and return None. This is finally a program that we can run!
  20. • Python/pythonrun.c - run_mod • Python/pythonrun.c - run_eval_code_obj • Python/ceval.c

    - PyEval_EvalCode • include/internal/pycore_ceval.h - _PyEval_EvalFrame • Python/ceval.c - _PyEval_EvalFrameDefault TARGET(LOAD_NAME) { frame->instr_ptr = next_instr; next_instr += 1; _PyStackRef v; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); _PyFrame_SetStackPointer(frame, stack_pointer); PyObject *v_o = _PyEval_LoadName(tstate, frame, name); stack_pointer[0] = v; stack_pointer += 1; PyBay 2025 – Noah Kantrowitz – @[email protected] 21 And so we go back up to run mod, this time with a fully compiled code object. Now we're looking for the part of Python which actually runs our bytecode. This leads us to _PyEval_EvalFrameDefault, which is possibly the most complex C function in all of Python. This function and its friends are the heart of Python, unpacking each compiled opcode and performing whatever computation it demands. This example here shows a bit of how LOAD_NAME works, it uses "py eval load name" to get the named thing and adds it to the stack.
  21. Aside: JIT PyBay 2025 – Noah Kantrowitz – @[email protected] 22

    This is also where the JIT happens. I wanted to show some code samples but the JIT is spread over so many places that there isn't any one thing to look at. The quick version is that when a JUMP_BACKWARD opcode is run, the executor checks if it looks like it's part of a frequently-used loop and if so, it pauses execution, cuts out the opcodes for the loop, converts them from Python opcodes to CPU instructions, and pastes them back in. It's complicated but very useful.
  22. What even is print? if (PyMapping_GetOptionalItem(frame->f_locals, name, &value) < 0)

    { return NULL; } if (value != NULL) { return value; } if (PyDict_GetItemRef(frame->f_globals, name, &value) < 0) { return NULL; } if (value != NULL) { return value; } if (PyMapping_GetOptionalItem(frame->f_builtins, name, &value) < 0) { return NULL; } PyBay 2025 – Noah Kantrowitz – @[email protected] 23 So let's execute our first opcode to load something named "print" onto the stack. Like saw two slides ago from the implementation of LOAD_NAME, it uses _PyEval_LoadName. In there we see the three lookup scopes which exist in Python, first check locals, then check globals, then check builtins. Our hello world doesn't declare any variables, either local or global so print must be coming from that third one, the builtins.
  23. • {"print", _PyCFunction_CAST(builtin_print), • Python/bltinmodule.c - builtin_print_impl if (file ==

    Py_None) { file = PySys_GetAttr(&_Py_ID(stdout)); } for (i = 0; i < objects_length; i++) { err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); if (err) { Py_DECREF(file); return NULL; } } PyBay 2025 – Noah Kantrowitz – @[email protected] 24 And indeed, in the list of builtin methods registed at startup, we find one named print. LOAD_NAME will push a function object to the stack, then we can run the LOAD_CONST opcode to push our hello world string, and then the CALL opcode to actually make the function call. We're making progress! Most of the code in "builtin print impl" deals with unpacking and validating arguments but deep down we can see some of the expected shape, it gets a reference to stdout from the sys module and calls "write object" on each argument in turn. We're 24 slides in, we've got to be close to seeing our Hello World, right?
  24. What is stdout? Python/pylifecycle.c - init_sys_streams extern FILE *stdout; /*

    Set sys.stdout */ fd = fileno(stdout); std = create_stdio(config, iomod, fd, 1, "<stdout>", config->stdio_encoding, config->stdio_errors); if (std == NULL) goto error; PySys_SetObject("__stdout__", std); _PySys_SetAttr(&_Py_ID(stdout), std); PyBay 2025 – Noah Kantrowitz – @[email protected] 25 If we're getting a reference to stdout from the sys module, where did that come from? When any program starts it recieves three special already- open files from its parent. These are standard in, out, and error, and they are numbered 0, 1, and 2 respectively. Python doesn't know anything about where standard out actually points, but it wraps it in a few encoding and buffering helpers and stores it in the sys module.
  25. Aside: Pythonic C writer = PyObject_GetAttr(f, &_Py_ID(write)); value = PyObject_Str(v);

    result = PyObject_CallOneArg(writer, value); Basically the same as ... writer = f.write value = str(v) result = write(value) PyBay 2025 – Noah Kantrowitz – @[email protected] 26 Another small aside, even though we're definitely down in the C code, you'll notice a lot of it reads very similarly to Python. There's other bits too, like manual error checks and refcounting but a helpful trick for reading C builtins is to mentally translate it to the equivalent Python.
  26. • Objects/fileobject.c - PyFile_WriteObject • Lib/_pyio.py - TextIOWrapper.write • Lib/_pyio.py

    - BufferedWriter.write • Modules/_io/fileio.c - _io_FileIO_write_impl • Python/fileutils.c - _Py_write_impl do { errno = 0; n = write(fd, buf, count); err = errno; } while (n < 0 && err == EINTR); PyBay 2025 – Noah Kantrowitz – @[email protected] 27 Once again let's fast forward down the stack a bit. We see each of those wrappers for encoding and buffering, and eventually end up in "py write impl". This is the core function used by all kinds of file IO in Python so it's covered in special cases for unusual operating systems but Linux is pretty simple, it calls write with the file descriptor for standard out which is 1, a buffer with our "hello world" bytes, and the number of bytes to write. This is the outer edge of Python, we could stop here but what fun would that be.
  27. Libc – A Standard Library for C POSIX too? But

    Really Glibc ssize_t write(int fildes, const void* buf, size_t nbyte); PyBay 2025 – Noah Kantrowitz – @[email protected] 28 To dive into the "write" function, we need to look at libc. Libc is a standardized API exposing helpers methods, operating system interfaces, math functions, and lots more. Libc itself is really just a specification, anyone can implement "write" as long as it matches this function declaraction. This API has been documented and formalized a few times but the most commonly used spec is the Portable Operating System Interface or POSIX. In most cases, the implementation we'll be using is GNU libc, which is shortened to glibc. For completeness, the other common libc is MUSL via Alpine container images.
  28. sysdeps/unix/sysv/linux/write.c - __libc_write /* Write NBYTES of BUF to FD.

    Return the number written, or -1. */ ssize_t __libc_write (int fd, const void *buf, size_t nbytes) { return SYSCALL_CANCEL (write, fd, buf, nbytes); } PyBay 2025 – Noah Kantrowitz – @[email protected] 29 This is the implementation of write in glibc. We see the expected the 3 arguments, the file descriptor we are writing to, the address of our bytes to write, and the number of those bytes to write. But the inside of this function isn't really doing very much, it's just forwarding our arguments into something named syscall, what's that?
  29. PyBay 2025 – Noah Kantrowitz – @[email protected] 30 Our programs

    can't be trusted. All normal programs run with a bunch of hardware-level security restrictions. For example, we can only access memory for our own program, we can't direct access someone else's memory. This is called "userspace", and while it's very important for digital security, sometimes we need to make some kind of dangerous call. Alongside our program there is the kernel, the part of the operating system running in the background. Unlike our programs, the kernel runs in "kernelspace", which can do more or less anything. So to come back to syscalls, a syscall is a request from our untrusted userspace program to run some kind of code in kernelspace on our behalf. You can think of this like the window at a bank teller, we can pass them a dollar bill but we can't jump over the counter.
  30. • sysdeps/unix/sysv/linux/write.c - __libc_write • nptl/cancellation.c - __internal_syscall_cancel • sysdeps/unix/sysv/linux/syscall_cancel.c

    - __syscall_cancel_arch • sysdeps/unix/sysdep.h - INTERNAL_SYSCALL_NCS_CALL • sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S movq %rdi, %rax /* Syscall number (1) -> rax. */ movq %rsi, %rdi /* shift arg1 - arg5. */ movq %rdx, %rsi movq %rcx, %rdx movq %r8, %r10 movq %r9, %r8 movq 8(%rsp),%r9 /* arg6 is on the stack. */ syscall /* Do the system call. */ PyBay 2025 – Noah Kantrowitz – @[email protected] 31 Glibc has a few wrapper functions to get through because write a special kind of "cancelable syscall", like in the case of a blocking disk write that has a timeout, but eventually we get to the syscall itself. This is written separately in assembly for each platform glibc supports. For x86 64, the standard is to put the syscall ID, which is 1 for write on our platform, in the rax CPU register. Then we put our 3 parameters to write into other CPU registers, and finally we run the syscall CPU instruction which tells the hardware to jump into kernel space.
  31. • arch/x86/entry/entry_64.S - entry_SYSCALL_64 • arch/x86/entry/syscall_64.c - x64_sys_call • fs/read_write.c

    - ksys_write struct files_struct *files = current->files; if (likely(atomic_read_acquire(&files->count) == 1)) { file = files_lookup_fd_raw(files, fd); if (!file || unlikely(file->f_mode & mask)) return EMPTY_FD; return BORROWED_FD(file); if (!fd_empty(f)) { loff_t pos, *ppos = file_ppos(fd_file(f)); ret = vfs_write(fd_file(f), buf, count, ppos); PyBay 2025 – Noah Kantrowitz – @[email protected] 32 As we land from the syscall CPU instruction, we're now inside the Linux kernel. There's a single landing pad for all syscalls which looks at that rax register and then dispatches to the correct function, for write that's "sys write", which is just a wrapper for "ksys write". Here we look up the requested file descriptor in the array of open files the kernel has been keeping on our behalf. The literal meaning of file descriptor 1 is that we want the second entry in that files array. Then we pass the file struct down to "vfs write".
  32. • arch/x86/entry/entry_64.S - entry_SYSCALL_64 • arch/x86/entry/syscall_64.c - x64_sys_call • fs/read_write.c

    - ksys_write • fs/read_write.c - vfs_write if (file->f_op->write) ret = file->f_op->write(file, buf, count, pos); else if (file->f_op->write_iter) ret = new_sync_write(file, buf, count, pos); else ret = -EINVAL; PyBay 2025 – Noah Kantrowitz – @[email protected] 33 Just like we dispatched to a callback function based on the syscall ID, we're going to dispatch again based on what kind of file we have. A million years ago, we said that our Python process received it's standard out from it's parent process at startup. In most cases that parent would be some kind of shell like bash or zsh, but what did it give us?
  33. >>> import os, stat >>> os.fstat(1).st_mode 8592 >>> stat.S_ISCHR(os.fstat(1).st_mode) True

    >>> os.fstat(1).st_rdev >> 8 136 >>> os.fstat(1).st_rdev & 0xFF 0 PyBay 2025 – Noah Kantrowitz – @[email protected] 34 The os dot fstat method let's us investigate any open file descriptor. Looking at standard out, we see it's got a funky file mode. Specifically that marks it as a device file, which is how Linux exposes drivers. Each device file has a device number, broken into a major and a minor. Here we have major 136 minor 0. This is new information but doesn't yet tell us what this file actually is.
  34. // include/uapi/linux/major.h #define UNIX98_PTY_MASTER_MAJOR 128 #define UNIX98_PTY_S***E_MAJOR 136 // drivers/tty/tty_io.c

    - tty_register_driver dev = MKDEV(driver->major, driver->minor_start); error = register_chrdev_region(dev, driver->num, driver->name); // drivers/tty/tty_io.c - tty_init static const struct file_operations tty_fops = { .read_iter = tty_read, .write_iter = tty_write, .splice_read = copy_splice_read }; cdev_init(&tty_cdev, &tty_fops); PyBay 2025 – Noah Kantrowitz – @[email protected] 35 To figure out where "vfs write" is going to send our call, we need to look at the device driver registrations inside the kernel. At startup, each subsystem in the kernel can register a driver with a given major, and then attach a struct of function pointers to it. Because our file has major 136, it's going to populate the f_ops value in the file with these functions. So we can see our write's next stop is "tty write".
  35. TTY - Teletype Terminal PyBay 2025 – Noah Kantrowitz –

    @[email protected] – Image by ArnoldReinhold 36 In the olden days, the way you interacted with a computer was through a modified electric typewriter called a teletype. This was a way to send input to a program by typing and then receive output from it by printing out to some paper. This one here is the Model 33, which was quite cutting edge for 1963. Teletype eventually got shortened to tty because our industry has literally always been like this.
  36. TTY - Teletype Terminal PTY - Pseudo TTY • A

    fancy pipe • A shared buffer • Bidirectional (but ignoring that today) static const struct tty_operations ptm_unix98_ops = { .open = pty_open, .close = pty_close, .write = pty_write, }; PyBay 2025 – Noah Kantrowitz – @[email protected] 37 But I'm guessing most of you don't work that way anymore. Usually now we have a software terminal of some kind, maybe xterm, Terminal.app, or cmd.exe. Because everything in computers is built on the bones of the past, we've left all the terminal interfaces the same since the 70s and just added on an emulated version. This is a pseudo tty, shortened to pty. For the most part it's a fancy pipe, anything you write on one side, of the pty can be read on the other. Internally that means it's a memory buffer that one side and write in and the other can read from. It's actually bidirectional so really there's one buffer for each direction but since we're only looking at output, we can skip that for now. This is what our standard out actually points to, a PTY device running in Unix98 compatbility mode.
  37. • arch/x86/entry/entry_64.S - entry_SYSCALL_64 • arch/x86/entry/syscall_64.c - x64_sys_call • fs/read_write.c

    - ksys_write • fs/read_write.c - vfs_write • drivers/tty/tty_io.c - tty_write • drivers/tty/tty_io.c - iterate_tty_write • drivers/tty/pty.c - pty_write • drivers/tty/tty_buffer.c - __tty_insert_flip_string_flags memcpy(char_buf_ptr(tb, tb->used), chars, space); PyBay 2025 – Noah Kantrowitz – @[email protected] 38 So back to our kernel call stack, we know "vfs write" will call into "tty write", which is then calling into "pty write". And going one layer deeper we reach the somewhat wordy "tty insert flip string flags". This helper method has a call to "mem copy". This has our hello world string and is ready to add it to the PTY's memory buffer. After all this work this is the final destination for our print!
  38. arch/x86/lib/memcpy_64.S - memcpy_orig SYM_FUNC_START_LOCAL(memcpy_orig) .Lless_16bytes /* * Move data from

    8 bytes to 15 bytes. */ movq 0*8(%rsi), %r8 movq -1*8(%rsi, %rdx), %r9 movq %r8, 0*8(%rdi) movq %r9, -1*8(%rdi, %rdx) RET PyBay 2025 – Noah Kantrowitz – @[email protected] 39 If you're familiar with C programming you may recognize memcpy as a libc function to copy bytes from one location in memory to another. But the kernel can't depend on libc, so it has it's own implementation in assembly. Hello world and a newline is 12 bytes, so we're going to end up in this section for copying between 8 and 15 bytes using 4 movq CPU instructions.
  39. Microcode? Transistors? Electrons? Quantum Mechanics? PyBay 2025 – Noah Kantrowitz

    – @[email protected] 40 We could go even deeper, looking at what transistors implement those MOVQ instructions, or how transistors work at all. But Intel and AMD don't share a lot of those details and it would have to get increasingly hand wavey. So I think this is our deepest level for today.
  40. We Rise • Execute RET instruction • Commit TTY write

    • Release VFS write lock • Return from syscall and then libc • Complete CALL opcode • Run LOAD_CONST None and RETURN_VALUE opcodes • Return from exec() • Loop and wait for input in InteractiveConsole.interact PyBay 2025 – Noah Kantrowitz – @[email protected] 41 With the write completed we rush back up the stack, seeing all our layers in reverse. Any kernel locks are cleaned up, we jump back to userspace, then finish the CALL opcode and so also the print function we've been inside for 25 minutes, and then the REPL loops again and waits for more input.
  41. // xterm size = (int) read(screen->respond, (char *) data->last, (size_t)

    // ... IChar *str = xw->work.write_text + offset; chars = ld->charData + screen->cur_col; for (n = 0; n < length; ++n) { chars[n] = str[n]; } PyBay 2025 – Noah Kantrowitz – @[email protected] 42 We know where the write ends up but how does it get to the screen? As we talked about before, PTYs are very fancy pipes so anything copied into one side can be read out on the other. Somewhere in your terminal program it will be reading in a loop, waiting for any new output from whatever is running. This makes a read syscall, diving back through libc and the kernel similarly to the write, eventually finding that same memory buffer as before and then it returns our hello world back up to the terminal program, which then renders it to the screen somehow. Exactly how the rendering works will vary but this is an example from xterm.
  42. $ python Python 3.14.0 (main) [Clang 17.0.0] on darwin Type

    "help", "credits" or "license" for more information. >>> print("Hello world") Hello world >>> PyBay 2025 – Noah Kantrowitz – @[email protected] 43 And so at long last, we can see our hello world exactly as expected. Simple, right?
  43. Why? PyBay 2025 – Noah Kantrowitz – @[email protected] 44 So

    why do we care about all of this? After all, probably everyone here could have told me what print hello world would do, this wasn't a puzzle to be solved.
  44. Abstractions Useful ... Until Not Be Prepared PyBay 2025 –

    Noah Kantrowitz – @[email protected] 45 Programming is full of amazing abstractions, and we couldn't build things without them. But there will come a day when every abstraction will fail you. Maybe your Python program is crashing in an unexpected way, or you need to improve performance on a webapp, or something new we haven't even thought of yet. Everyone should feel comfortable peeling back the layers until you find what you need. This was certainly overkill for dramatic effect, but over my career I've needed to poke around in every one of these projects except maybe xterm.
  45. Open Source Is An Open Door PyBay 2025 – Noah

    Kantrowitz – @[email protected] 46 Play with your tools, tinker, fix, experiment. You aren't just a Python developer, you're an everything developer and inside every abstraction is a new lesson to learn.