How to Find Out What's Filling Up Your Disk in Ubuntu with a Custom Python Script
It happens to everyone: out of nowhere, Ubuntu warns you that you are running out of disk space. The problem is that manually hunting down which massive folders or residual files are devouring storage inside your home directory (~) can be an exhausting and boring task.
While graphical tools like Baobab (Disk Usage Analyzer) exist, terminal enthusiasts often prefer fast, customizable, and native solutions.
In this tutorial, we will present an ultra-elegant Python script that scans your Home directory, dynamically calculates the actual size of your files and folders, and displays them in beautifully organized ASCII tables sorted from largest to smallest.
⚠️ An Important Note on Safety: 100% Informational
Before we begin, we want to give you complete peace of mind: this script is 100% SAFE.
Read also
- Read-only: The code contains absolutely no deletion instructions (os.remove, shutil.rmtree, etc.).
- Exclusive Purpose: Its sole function is to visualize and analyze your disk space status.
- You remain in control: Once the script shows you which files and folders are the heaviest, you will be the one to decide, manually and consciously, if you want to delete anything via your file manager or the terminal.
Key Features of the Script
- Real-time Loading Animation (Spinner): While processing thousands of files, you will see a dynamic visual indicator in your terminal so you know it is actively working.
- Elegant ASCII Tables: Results are not thrown at you as boring plain text; they are organized into clean tables styled with native terminal colors.
- Smart Size Filter (min_mb): You can tell it to ignore small files or folders (for example, anything under 100 MB) so you can focus exclusively on the true storage hogs.
- Accurate Calculation: Unlike basic scripts, this one calculates folder weights from the bottom up (topdown=False), correctly summing up the weight of all nested subfolders.
The Complete Code
Create a file named analyze_space.py and paste the following code:
Python
import os
import time
import sys
import threading
import itertools
from collections import defaultdict
# --- 1. CLASS FOR NATIVE TERMINAL COLORS ---
class Colors:
PURPLE = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BLACK = '\033[90m'
BOLD = '\033[1m'
RESET = '\033[0m'
# --- 2. CLASS FOR THE LOADING ANIMATION (SPINNER) ---
class Spinner:
def __init__(self, message="Processing..."):
self.spinner = itertools.cycle(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'])
self.stop_event = threading.Event()
self.message = message
self.thread = threading.Thread(target=self._spin)
def _spin(self):
while not self.stop_event.is_set():
sys.stdout.write(f"\r{Colors.CYAN}{Colors.BOLD}{next(self.spinner)} {self.message}{Colors.RESET}")
sys.stdout.flush()
time.sleep(0.1)
sys.stdout.write('\r' + ' ' * (len(self.message) + 5) + '\r')
def start(self):
self.thread.start()
def stop(self):
self.stop_event.set()
self.thread.join()
# --- 3. DYNAMIC ASCII TABLE DRAWING ---
def print_table(title, data, border_color):
size_width = 12
if data:
path_width = max(len(path) for _, path in data)
else:
print(f"{border_color}No elements found that exceed the minimum size.{Colors.RESET}\n")
return
min_total_width = len(title)
if (size_width + path_width) < min_total_width:
path_width = min_total_width - size_width
print(f"{border_color}┌{'─' * (size_width + 2)}┬{'─' * (path_width + 2)}┐{Colors.RESET}")
print(f"{border_color}│ {Colors.BOLD}{title.center(size_width + path_width + 3)}{Colors.RESET}{border_color} │{Colors.RESET}")
print(f"{border_color}├{'─' * (size_width + 2)}┼{'─' * (path_width + 2)}┤{Colors.RESET}")
print(f"{border_color}│ {Colors.BOLD}{'Size'.center(size_width)}{Colors.RESET}{border_color} │ {Colors.BOLD}{'Path'.ljust(path_width)}{Colors.RESET}{border_color} │{Colors.RESET}")
print(f"{border_color}├{'─' * (size_width + 2)}┼{'─' * (path_width + 2)}┤{Colors.RESET}")
for size, path in data:
path_printed = path.ljust(path_width)
print(f"{border_color}│ {Colors.GREEN}{Colors.BOLD}{size:>10}{Colors.RESET}{border_color} │ {Colors.RESET}{path_printed} {border_color}│{Colors.RESET}")
print(f"{border_color}└{'─' * (size_width + 2)}┴{'─' * (path_width + 2)}┘{Colors.RESET}\n")
def format_size(size_bytes):
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} PB"
# --- 4. ANALYSIS LOGIC ---
def analyze_home_directory(top_n=15, min_mb=0):
start_time = time.time()
home = os.path.expanduser("~")
file_list = []
folder_sizes = defaultdict(int)
# Convert Megabytes to Bytes for Python to compare them
min_bytes = min_mb * 1024 * 1024
print("\n")
spinner = Spinner(f"Scanning your folder: {home} (Filter > {min_mb} MB)")
spinner.start()
try:
for root, directories, files in os.walk(home, topdown=False):
local_files_size = 0
for file in files:
full_path = os.path.join(root, file)
if os.path.islink(full_path):
continue
try:
size = os.path.getsize(full_path)
local_files_size += size
# Filter files before adding them to the list
if size >= min_bytes:
file_list.append((full_path, size))
except (PermissionError, FileNotFoundError):
continue
subfolders_size = 0
for d in directories:
subfolder_path = os.path.join(root, d)
subfolders_size += folder_sizes.get(subfolder_path, 0)
# Calculate total folder size
total_folder_size = local_files_size + subfolders_size
# Filter folders before saving them to the dictionary
if total_folder_size >= min_bytes:
folder_sizes[root] = total_folder_size
finally:
spinner.stop()
heavy_files = sorted(file_list, key=lambda x: x[1], reverse=True)[:top_n]
heavy_folders = sorted(folder_sizes.items(), key=lambda x: x[1], reverse=True)[:top_n]
file_data = [(format_size(t), r) for r, t in heavy_files]
folder_data = [(format_size(t), r) for r, t in heavy_folders]
# --- OUTPUT RESULTS ---
print_table(f"📂 TOP {top_n} HEAVIEST FOLDERS (> {min_mb} MB)", folder_data, Colors.PURPLE)
print_table(f"📄 TOP {top_n} HEAVIEST FILES (> {min_mb} MB)", file_data, Colors.BLUE)
# --- TIME CALCULATION AND FORMATTING ---
end_time = time.time()
duration = end_time - start_time
minutes = int(duration // 60)
seconds = duration % 60
if minutes > 0:
time_text = f" {minutes} min and {seconds:.2f} sec "
else:
time_text = f" {seconds:.2f} sec "
print(f"{Colors.YELLOW}╭{'─' * (len(time_text) + 24)}╮{Colors.RESET}")
print(f"{Colors.YELLOW}│ {Colors.BOLD}⏱️ Total execution time:{Colors.RESET} {time_text}{Colors.YELLOW}│{Colors.RESET}")
print(f"{Colors.YELLOW}╰{'─' * (len(time_text) + 24)}╯{Colors.RESET}\n")
if __name__ == "__main__":
# ---> ADJUST YOUR CONFIGURATION HERE <---
# top_n: number of items to display in the table.
# min_mb: ignores folders/files smaller than this size in Megabytes.
analyze_home_directory(top_n=50, min_mb=100)
How to Run the Script in Ubuntu (Step-by-Step)
Since Ubuntu comes with Python 3 pre-installed natively, you do not need to configure complex virtual environments or install external libraries via pip. Everything is built using standard Python modules.
Read also
- Open the Terminal: Use the keyboard shortcut Ctrl + Alt + T.
- Create the script file: Use your preferred terminal editor, such as nano:
Bash
nano analyze_space.py
- Paste the code: Copy the script above and paste it inside the terminal (you can use Ctrl + Shift + V in Ubuntu).
- Save and exit: In nano, press Ctrl + O followed by Enter to save, and then Ctrl + X to exit.
- Run the script: Execute the analysis with the following command:
Bash
python3 analyze_space.py
Understanding the Internal Workings
The core of this script relies on the os.walk(home, topdown=False) function.
- Why topdown=False? This is crucial. It means Python will traverse the folders from the deepest levels (the most hidden subfolders) upward. By doing it this way, the script can calculate the weight of inner files and pass that exact accumulated value up to parent folders recursively.
- Using Threads (threading): Scanning an entire drive can take a few seconds or minutes depending on your file count. To prevent the terminal from looking frozen, the Spinner class handles the loading animation on a secondary thread, keeping the interface alive while the main thread performs the heavy directory scanning.
How to Customize Your Scans
If you scroll down to the bottom of the file, you will find this line:
Python
analyze_home_directory(top_n=50, min_mb=100)
You can edit these parameters based on your current needs:
- If you want to see a Top 10 instead of 50, change top_n=10.
- If you have very large drives and only care about massive files, you can increase the threshold by changing min_mb=500 (to search only for items over 500 MB) or even min_mb=1000 (for items over 1 GB).
Conclusion and Manual Cleanup
Once the analysis completes, you will see highly visual tables detailing the exact paths of bloated cache folders, old downloads, heavy virtual environments, or long-forgotten video files.
Because this script is completely passive and harmless, it is now up to you to open your file manager (Ubuntu Files) or use commands like rm manually and precisely to delete only what you are 100% sure you no longer need. Happy disk cleaning!



