Every program runs somewhere. It lives in a folder on disk, starts when you launch it, and reads options the operating system hands it. Up to now your code ignored all of that — you worked with values that lived only in memory. Real programs reach outward: they build the path to a file, ask which Python is running, and read the arguments they were started with.
Python gives you two modules for exactly this. os is your bridge to the operating system, and above all to paths — the strings that name where files live. sys is your window onto the interpreter itself: which version is running, on what platform, and how it was launched. Together they are how a script learns about the world it runs in.
flowchart LR
subgraph OS["os - the system outside"]
P["os.path joins and splits paths"]
end
subgraph SYS["sys - the interpreter within"]
V["version, platform, argv"]
end
style OS fill:#3776ab,color:#fff
style SYS fill:#b45309,color:#fff
Building paths without hard-coding slashes
A path looks like plain text — docs/notes/report.txt — and the slash is the whole problem. On Linux and macOS the folder separator is a forward slash; on Windows it is a backslash. Glue parts together with + and a hard-coded slash, and your script works on your machine but silently builds a broken path for a colleague on the other system.
os.path.join solves it. Hand it the pieces and it inserts the correct separator for whatever platform the code runs on:
import os
os.path.join('docs', 'notes', 'report.txt')
The call returns a single path string built with this system's separator. The same line produces a forward-slash path on one platform and a backslash path on another — and both are correct, because each was assembled for the system that will use it. Joining is a pure string operation; no file is ever opened or read.
import os
p = os.path.join('docs', 'notes', 'report.txt')
print(p)
print('name:', os.path.basename(p))
print('folder:', os.path.dirname(p))
Taking a path apart
If joining assembles a path, three functions take one to pieces. os.path.basename returns the final component — the file's name alone. os.path.dirname returns everything before it — the folder that contains it. os.path.splitext splits a name into its root and its extension, handing back a two-item tuple.
os.path.basename('docs/notes/report.txt') # 'report.txt'
os.path.dirname('docs/notes/report.txt') # 'docs/notes'
os.path.splitext('report.tar.gz') # ('report.tar', '.gz')
Notice the last result. splitext peels off only the final dot, so report.tar.gz keeps .tar with the root and yields .gz as the extension. A naive name.split('.')[-1] agrees here — but it fails the moment a filename has no dot, returning the whole name instead of an empty extension. Reach for the function built for the job.
flowchart LR A["report.tar.gz"] --> S["os.path.splitext"] S --> R["report.tar"] S --> E[".gz"] style S fill:#3776ab,color:#fff style E fill:#b45309,color:#fff
import os
name = 'archive.tar.gz'
root, ext = os.path.splitext(name)
print('root:', root)
print('ext:', ext)
print('upper:', ext.upper())
Pairs that come back as tuples
Several os.path functions hand you two related values at once, packed as a tuple. os.path.split(path) returns the dirname and basename pair together; os.path.splitext(name) returns the root and the extension. Python lets you unpack either straight into two names, which reads like a sentence and saves the index you would otherwise write:
folder, file = os.path.split('docs/report.txt') # ('docs', 'report.txt')
The same shape appears all over the standard library. A function that finds two things tends to return them as a tuple, and Python expects you to pull them apart by position — so unpacking is a habit worth forming early.
String operations, not disk access
Here is the quiet guarantee that makes os.path safe to practise with: not one of these functions touches the disk. join, split, basename, dirname, and splitext are pure string operations. You hand them text, they hand back text, and no file is opened. That is why they are fast, deterministic, and testable — and why a lesson can run them inside a browser sandbox.
The same module does reach the disk, through functions like os.path.exists, os.listdir, and os.getcwd. Those answer real questions — does this file exist, what sits in this folder, where am I — but their answers depend on the machine and the moment, so they are non-deterministic. Use the string helpers to build and reason about a path, and reach for the disk helpers only when you must act on the real filesystem.
sys: the interpreter that runs your code
Where os looks outward at the system, sys looks inward at Python itself. A handful of attributes answer the questions every script eventually asks.
sys.platform names the operating system family — linux, win32, or darwin for macOS. Branch on it only when behaviour genuinely differs between platforms. sys.version_info exposes the Python version as attributes: .major is 3, and .minor the release. sys.executable is the path to the Python currently running, handy when a tool must launch Python again. sys.path is the list of folders Python searches when you import a module, and sys.argv holds the arguments the program was started with, as a list whose first entry is the script's own name.
import sys
print(sys.platform)
print(sys.version_info.major, sys.version_info.minor)
Reading these costs nothing and never fails, so they are safe to inspect anywhere — in the example above, or deep in production code that needs to know its own surroundings.
import sys
print('platform:', sys.platform)
print('python major:', sys.version_info.major)
print('executable:', sys.executable)
flowchart TD C["hard-coded slash"] -->|works on one OS| W["breaks on another"] J["os.path.join"] -->|uses the right separator| OK["works everywhere"] style C fill:#b45309,color:#fff style J fill:#3776ab,color:#fff
Write build_path(folder, filename) that joins a folder and a filename into a single path using os.path.join. It must work on any operating system.
import os
def build_path(folder, filename):
# join folder and filename into one path
pass
os.path.join(folder, filename) returns the joined path.
The tests compare against os.path.join itself, so the separator is correct on every platform.
import os
def build_path(folder, filename):
return os.path.join(folder, filename)
Read it carefully and predict both lines. Remember which function returns the file and which returns the folder.
import os p = 'logs/2026/error.log' print(os.path.basename(p)) print(os.path.dirname(p))
basename returns the final component - the file's name.
dirname returns everything before that final separator.
This function glues a folder and filename together with a hard-coded forward slash. It works on Linux but builds a broken path on Windows. Fix it to use os.path.join so the correct separator is chosen on every platform.
import os
def join_path(folder, filename):
return folder + "/" + filename # bug: hard-coded slash
Replace the string concatenation with a single call to os.path.join.
Pass folder and filename as the two arguments; join inserts the right separator.
import os
def join_path(folder, filename):
return os.path.join(folder, filename)
Write extension_of(filename) that returns a file's extension, including the leading dot, using os.path.splitext. For report.tar.gz it must return .gz - only the final extension counts. A filename with no dot returns an empty string.
import os
def extension_of(filename):
# splitext returns (root, ext); return the extension
pass
os.path.splitext(filename) returns a tuple: (root, ext).
Index the second element with [1] - it holds the extension, dot included.
import os
def extension_of(filename):
return os.path.splitext(filename)[1]
What does os.path.join('/home', '/tmp') return?
When a later argument is an absolute path - it starts with a separator - os.path.join discards everything before it. So '/tmp' wins and the result is '/tmp', not '/home/tmp'. That silent discard is the classic os.path.join trap.
Recap
osbridges to the operating system;sysreflects the interpreter. Reach foroswhen you work with paths, andsyswhen you need the version or platform.- Build paths with
os.path.joinso the separator suits the platform, and never hard-code a slash. - Take paths apart with
basename(the file),dirname(the folder), andsplitext(root plus final extension). - The
os.pathhelpers are pure string operations - fast and deterministic; onlyexists,listdir, and their cousins touch the real disk. - An absolute path passed to
os.path.joinresets the result - a silent trap when your pieces might already be rooted.
Next you will turn these foundations into idiomatic, professional Python - the conventions experienced developers reach for without a second thought.