{"id":926,"date":"2024-09-25T21:59:08","date_gmt":"2024-09-25T21:59:08","guid":{"rendered":"https:\/\/algocademy.com\/blog\/mastering-the-python-ls-command-a-comprehensive-guide-to-listing-files-in-directories\/"},"modified":"2026-10-15T10:34:50","modified_gmt":"2026-10-15T10:34:50","slug":"mastering-the-python-ls-command-a-comprehensive-guide-to-listing-files-in-directories","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/mastering-the-python-ls-command-a-comprehensive-guide-to-listing-files-in-directories\/","title":{"rendered":"Python ls: How to List Files in a Directory (os, pathlib, glob)"},"content":{"rendered":"<p>Python has no built-in <code>ls<\/code>. To list the files in a directory, use <code>os.listdir()<\/code> for a quick list of names, <code>pathlib.Path.iterdir()<\/code> for modern code, <code>glob<\/code> or <code>Path.glob()<\/code> to match patterns like <code>*.py<\/code>, and <code>os.scandir()<\/code> when you also need file sizes or types. The examples here were run with Python 3 on Linux, against this folder:<\/p>\n<pre><code class=\"language-bash\">demo\/\n  a.txt\n  b.py\n  .hidden\n  .git\/\n  sub\/\n    c.py<\/code><\/pre>\n<h2>os.listdir: the plain list of names<\/h2>\n<pre><code class=\"language-python\">import os\n\nprint(sorted(os.listdir(\"demo\")))\n# ['.git', '.hidden', 'a.txt', 'b.py', 'sub']<\/code><\/pre>\n<p><code>os.listdir(path)<\/code> returns a list of names (not full paths) of everything in the folder, files and subfolders alike, <strong>including hidden files<\/strong>. It never includes <code>.<\/code> and <code>..<\/code>. With no argument it lists the current directory.<\/p>\n<p>The order is arbitrary, whatever the operating system returns. If order matters, wrap it in <code>sorted()<\/code>, as above. To drop hidden entries, as plain <code>ls<\/code> does:<\/p>\n<pre><code class=\"language-python\">visible = [n for n in sorted(os.listdir(\"demo\")) if not n.startswith(\".\")]\nprint(visible)\n# ['a.txt', 'b.py', 'sub']<\/code><\/pre>\n<h2>pathlib: the modern way<\/h2>\n<p><code>pathlib<\/code> (standard library since Python 3.4) gives you <code>Path<\/code> objects instead of strings, so you can chain checks and joins.<\/p>\n<pre><code class=\"language-python\">from pathlib import Path\n\np = Path(\"demo\")\nprint(sorted(x.name for x in p.iterdir()))\n# ['.git', '.hidden', 'a.txt', 'b.py', 'sub']\n\nprint(sorted(x.name for x in p.glob(\"*.py\")))\n# ['b.py']\n\nprint(sorted(str(x) for x in p.rglob(\"*.py\")))\n# ['demo\/b.py', 'demo\/sub\/c.py']\n\nprint(sorted(str(x) for x in p.glob(\"*\") if x.is_file()))\n# ['demo\/.hidden', 'demo\/a.txt', 'demo\/b.py']<\/code><\/pre>\n<ul>\n<li><code>iterdir()<\/code> yields every entry, including hidden ones, as a full <code>Path<\/code>.<\/li>\n<li><code>glob(\"*.py\")<\/code> matches in that folder only.<\/li>\n<li><code>rglob(\"*.py\")<\/code> searches the folder and all subfolders. It is the same as <code>glob(\"**\/*.py\")<\/code>.<\/li>\n<li><code>is_file()<\/code> and <code>is_dir()<\/code> let you keep only files or only folders.<\/li>\n<\/ul>\n<p>Notice that <code>glob(\"*\")<\/code> still returned <code>demo\/.hidden<\/code> in the last example. Hidden-file handling in <code>glob<\/code> differs from the shell, as the next section shows.<\/p>\n<h2>glob: patterns, shell style<\/h2>\n<pre><code class=\"language-python\">import glob\n\nprint(sorted(glob.glob(\"demo\/*.py\")))\n# ['demo\/b.py']\n\nprint(sorted(glob.glob(\"demo\/**\/*.py\", recursive=True)))\n# ['demo\/b.py', 'demo\/sub\/c.py']\n\nprint(sorted(glob.glob(\"demo\/*\")))\n# ['demo\/a.txt', 'demo\/b.py', 'demo\/sub']\n\nprint(sorted(glob.glob(\"demo\/.*\")))\n# ['demo\/.git', 'demo\/.hidden']<\/code><\/pre>\n<p>The <code>glob<\/code> module follows shell rules: <code>*<\/code> does not match names that start with a dot, so hidden files appear only when you ask for them with a pattern like <code>.*<\/code>. This is the closest match to what <code>ls demo\/*<\/code> does in a terminal. The <code>**<\/code> pattern needs <code>recursive=True<\/code>, otherwise <code>**<\/code> acts like a single <code>*<\/code>.<\/p>\n<h2>os.scandir: names plus file info, without extra calls<\/h2>\n<p>If you need sizes, types or modification times, <code>os.scandir()<\/code> is the efficient choice. Each entry comes with type information already known, so checking <code>is_dir()<\/code> usually does not need another system call, which matters on large folders.<\/p>\n<pre><code class=\"language-python\">import os\n\nwith os.scandir(\"demo\") as it:\n    for e in sorted(it, key=lambda e: e.name):\n        print(e.name, e.is_dir(), e.is_file(), e.stat().st_size if e.is_file() else \"-\")\n# .git True False -\n# .hidden False True 1\n# a.txt False True 5\n# b.py False True 4\n# sub True False -<\/code><\/pre>\n<p>Use it as a context manager (<code>with<\/code>) so the directory handle closes cleanly. The entries are <code>DirEntry<\/code> objects, not strings: use <code>e.name<\/code> for the name and <code>e.path<\/code> for the path.<\/p>\n<h2>A small ls -l in Python<\/h2>\n<pre><code class=\"language-python\">import os\nfrom datetime import datetime\n\ndef ls_l(path=\".\"):\n    for e in sorted(os.scandir(path), key=lambda e: e.name.lower()):\n        if e.name.startswith(\".\"):\n            continue                      # skip hidden, like plain ls\n        st = e.stat()\n        kind = \"d\" if e.is_dir() else \"-\"\n        when = datetime.fromtimestamp(st.st_mtime).strftime(\"%Y-%m-%d\")\n        print(f\"{kind} {st.st_size:&gt;8} {when} {e.name}\")\n\nls_l(\"demo\")\n# -        5 2026-10-15 a.txt\n# -        4 2026-10-15 b.py\n# d     4096 2026-10-15 sub<\/code><\/pre>\n<p>Your dates will differ. The size shown for a folder is how much space the folder entry itself takes, not what is inside it, and the number depends on the file system.<\/p>\n<h2>Walking a whole tree with os.walk<\/h2>\n<p>When you need every file below a folder, <code>os.walk()<\/code> gives you the folder, its subfolders and its files at each level. Changing the <code>dirs<\/code> list in place stops it from descending into folders you want to skip, such as <code>.git<\/code>:<\/p>\n<pre><code class=\"language-python\">import os\n\nfor folder, dirs, files in os.walk(\"demo\"):\n    dirs[:] = [d for d in dirs if not d.startswith(\".\")]   # skip hidden folders\n    for name in sorted(files):\n        print(os.path.join(folder, name))\n# demo\/.hidden\n# demo\/a.txt\n# demo\/b.py\n# demo\/sub\/c.py<\/code><\/pre>\n<p>To list the newest files first, sort by modification time: <code>sorted(Path(\"demo\").iterdir(), key=lambda p: p.stat().st_mtime, reverse=True)<\/code>. This is the equivalent of <code>ls -t<\/code>.<\/p>\n<h2>Which one should you use?<\/h2>\n<table>\n<thead>\n<tr>\n<th scope=\"col\">Need<\/th>\n<th scope=\"col\">Use<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Quick list of names<\/td>\n<td><code>os.listdir(path)<\/code><\/td>\n<\/tr>\n<tr>\n<td>New code, path joining, checks<\/td>\n<td><code>Path(path).iterdir()<\/code><\/td>\n<\/tr>\n<tr>\n<td>Only files matching a pattern<\/td>\n<td><code>Path.glob()<\/code> or <code>glob.glob()<\/code><\/td>\n<\/tr>\n<tr>\n<td>Everything under a folder<\/td>\n<td><code>Path.rglob()<\/code> or <code>os.walk()<\/code><\/td>\n<\/tr>\n<tr>\n<td>Sizes, types or times for many files<\/td>\n<td><code>os.scandir()<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Errors you will hit<\/h2>\n<pre><code class=\"language-python\">import os\n\nfor bad in [\"nope\", \"demo\/a.txt\"]:\n    try:\n        os.listdir(bad)\n    except Exception as ex:\n        print(type(ex).__name__, ex)\n# FileNotFoundError [Errno 2] No such file or directory: 'nope'\n# NotADirectoryError [Errno 20] Not a directory: 'demo\/a.txt'<\/code><\/pre>\n<p>In real code catch the specific ones: <code>FileNotFoundError<\/code> (path does not exist), <code>NotADirectoryError<\/code> (it is a file), and <code>PermissionError<\/code> (you cannot read it). The most common bug is a <strong>relative path that depends on where the script runs<\/strong>. If a script works in your editor and fails from the terminal, print <code>os.getcwd()<\/code>, or build paths from the script&#8217;s own location with <code>Path(__file__).parent<\/code>.<\/p>\n<p>Another common bug: <code>os.listdir()<\/code> returns names, not paths. <code>os.path.getsize(name)<\/code> fails unless the current directory is the folder you listed. Join first with <code>os.path.join(folder, name)<\/code>, or use <code>pathlib<\/code> or <code>scandir<\/code>, which give you full paths.<\/p>\n<h2>Calling the real ls<\/h2>\n<p>You can run the system&#8217;s <code>ls<\/code> from Python with <code>subprocess<\/code>, but it is rarely a good idea: it breaks on Windows, and you have to parse text. If you must:<\/p>\n<pre><code class=\"language-python\">import subprocess\n\nr = subprocess.run([\"ls\", \"-1A\", \"demo\"], capture_output=True, text=True, check=True)\nprint(sorted(r.stdout.split()))\n# ['.git', '.hidden', 'a.txt', 'b.py', 'sub']<\/code><\/pre>\n<p>In Jupyter and IPython, <code>%ls<\/code> and <code>!ls<\/code> work as shortcuts, but they are notebook features, not Python.<\/p>\n<h2>Practice<\/h2>\n<p>Listing files is a small loop over a list, so the practice is the same as for any loop: filter, sort, and build a result. If that part is shaky, work through the <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=arrays&amp;solution=1\">lists lesson<\/a> and the <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=functions&amp;solution=1\">functions lesson<\/a> in the AlgoCademy Python course. For a related Python list task, see <a href=\"https:\/\/algocademy.com\/blog\/remove-none-from-list-in-python-a-comprehensive-guide\/\">removing None from a list in Python<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Python has no ls command, but os.listdir, os.scandir, pathlib and glob do the same job. Learn which to use, how to sort, filter, go recursive and handle errors.<\/p>\n","protected":false},"author":1,"featured_media":1379,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-926","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/926"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=926"}],"version-history":[{"count":2,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/926\/revisions"}],"predecessor-version":[{"id":8750,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/926\/revisions\/8750"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/1379"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=926"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=926"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=926"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}