Python on a Chromebook
From first script to GUI apps, web services, and data science — entirely inside the ChromeOS Linux container.
Overview & Your Options
Python is an ideal language for Chromebook development. The ChromeOS Linux container ships with Python 3 pre-installed — you can open a terminal and start coding immediately. GUI apps appear as native ChromeOS windows via XWayland, Jupyter Notebook opens directly in Chrome, and web apps are accessible at localhost without any configuration.
python3 — it's already there. The minimal extra setup is just pip3 and python3-venv.Python on Chromebook vs Other Platforms
| Feature | Chromebook Linux | Termux | Google Colab |
|---|---|---|---|
| Python version | 3.11+ (Debian Bookworm) | 3.11+ | 3.10+ |
| pip / packages | Full PyPI access | Most packages | pip in cells |
| GUI (Tkinter, GTK3) | ✓ Native ChromeOS windows | ✗ | ✗ |
| Jupyter Notebook | ✓ in Chrome browser | Limited | ✓ Native |
| Web server (Flask) | ✓ localhost:5000 | ✓ localhost | Ngrok needed |
| File access | ✓ Files app integration | Limited | Google Drive |
| Offline use | ✓ Fully offline | ✓ | ✗ |
Enable the Linux Container
ChromeOS's Linux container (Crostini) provides a complete Debian environment. Enabling it takes two minutes.
Click the clock (bottom-right) → gear icon. Navigate to Advanced → Developers → Linux development environment → Turn on.
Select 10–20 GB (more if you plan to use data science libraries and datasets). Click Install. First-time setup takes 3–10 minutes.
python3 --version # Python 3.11.x or 3.12.x
python3 -c "import sys; print(sys.prefix)"
which python3 # /usr/bin/python3In the Files app, right-click Downloads → Share with Linux. Your Downloads then appear at /mnt/chromeos/MyFiles/Downloads.
Install Python Tools & Libraries
Python 3 is already present. This section installs the essential developer tools around it.
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
python3 \
python3-pip \
python3-venv \
python3-dev \
python3-tk \
python3-doc \
build-essential \
libssl-dev \
libffi-dev
# Verify
python3 -m pip --version # pip 23.x
python3 -m venv --help | head -2# GTK3 via PyGObject
sudo apt install -y \
python3-gi \
python3-gi-cairo \
gir1.2-gtk-3.0 \
gir1.2-glib-2.0
# PyQt5 (optional, for Qt-style apps)
pip3 install PyQt5 --user
# Test Tkinter
python3 -c "import tkinter; print('Tkinter', tkinter.TkVersion, '— OK')"
# Test GTK3
python3 -c "
import gi; gi.require_version('Gtk','3.0')
from gi.repository import Gtk
print('GTK3 OK')
"pip3 install --user \
flask \
fastapi \
uvicorn \
requests \
httpx \
beautifulsoup4 \
lxmlpip3 install --user \
jupyter \
jupyterlab \
pandas \
numpy \
matplotlib \
seaborn \
scikit-learn \
openpyxl
# Launch Jupyter (opens in Chrome automatically)
jupyter notebookpip3 install --user \
black \
pylint \
mypy \
pytest \
ipython \
rich \
typer
# Verify
black --version # black, 24.x
pytest --version # pytest 8.x
ipython --version # 8.xVirtual Environments
A virtual environment is an isolated Python installation for a single project. It keeps dependencies separate so projects don't conflict. In Python, this is best practice for every non-trivial project.
# Create a new project with its own environment
mkdir ~/projects/my_app && cd ~/projects/my_app
python3 -m venv .venv # Create venv in .venv/ folder
# Activate (do this every time you open a terminal for this project)
source .venv/bin/activate # Prompt changes to (.venv) user@penguin:~/projects/my_app$
# Now pip installs ONLY into this project's venv
pip install flask pandas # Installed into .venv/lib/
# Check what's installed
pip list
pip freeze > requirements.txt # Save the dependency list
# Deactivate when done
deactivate
# Recreate from requirements.txt (on another machine or after deletion)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtVS Code + venv Integration
# VS Code auto-detects .venv in the workspace folder
code ~/projects/my_app # Open VS Code in the project
# Bottom-left of VS Code: click Python version → select .venv/bin/python
# VS Code then uses that venv for IntelliSense, linting, and running scriptsUsing pyenv for Multiple Python Versions
# Install pyenv (manages multiple Python versions)
curl https://pyenv.run | bash
# Add to ~/.bashrc:
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
# Install a specific Python version
pyenv install 3.12.4
pyenv install 3.11.9
# Set versions
pyenv global 3.12.4 # Default for all new terminals
pyenv local 3.11.9 # Just for the current directory
python3 --version # Confirms which version is activeEditors & IDE Setup
VS Code (Recommended)
sudo apt install -y wget gpg
wget -qO- https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor > /tmp/microsoft.gpg
sudo install -D -o root -g root -m 644 \
/tmp/microsoft.gpg /etc/apt/keyrings/microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64,arm64 \
signed-by=/etc/apt/keyrings/microsoft.gpg] \
https://packages.microsoft.com/repos/code stable main" \
> /etc/apt/sources.list.d/vscode.list'
sudo apt update && sudo apt install -y code
code . # Opens VS Code as a ChromeOS windowEssential VS Code Python Extensions
The core Python extension — IntelliSense, debugging, linting, test runner. Install first.
Fast type-checking language server. Auto-installed with the Python extension.
Run .ipynb notebooks directly inside VS Code.
Auto-format on save with Black. Best Python formatter.
Correct indentation handling — critical for Python.
Git integration — useful when committing Python projects.
# Install from command line
code --install-extension ms-python.python
code --install-extension ms-python.pylance
code --install-extension ms-toolsai.jupyter
code --install-extension ms-python.black-formatter
# Create a project settings file (auto-formats on save)
mkdir -p ~/projects/my_app/.vscode
cat > ~/projects/my_app/.vscode/settings.json << 'EOF'
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.linting.enabled": true,
"python.linting.pylintEnabled": true
}
EOFThonny — Beginner-Friendly IDE
Thonny is a simple Python IDE designed for learners. It has a built-in debugger that shows variable values visually — great for learning.
sudo apt install -y thonny
thonny # Opens as a ChromeOS window — simple, clean IDEIPython — Enhanced Interactive Shell
pip3 install --user ipython
ipython # Drop into an enhanced REPL with tab-completion and magic commands
# Useful IPython magic commands:
# %timeit my_function() — Time execution
# %run script.py — Run a file
# %history — Show command history
# ?my_object — Show documentation
# ??my_function — Show source codeDevelopment Workflow
python3 script.py # Run a script
python3 -c "print('hello')" # One-liner
python3 -m module_name # Run a module as a script
# Syntax check (without running)
python3 -m py_compile script.py
# Type checking
mypy script.py # Static type analysis
# Formatting
black script.py # Auto-format (modifies in-place)
black --check script.py # Check without changing
# Linting
pylint script.py # Detailed code analysis
flake8 script.py # Fast style checker
# Testing
pytest # Discover and run all tests
pytest tests/test_app.py # Run specific test file
pytest -v # Verbose output
# Interactive debugging
python3 -m pdb script.py # Built-in debugger
python3 -m pdb -c continue script.py # Run until error, then drop to debuggerFiles App Integration
# Access ChromeOS Downloads from the Linux container:
ls /mnt/chromeos/MyFiles/Downloads/
# Copy a downloaded .py file into your project:
cp /mnt/chromeos/MyFiles/Downloads/script.py ~/projects/
# Save output to Downloads (opens in ChromeOS Files):
python3 report.py > /mnt/chromeos/MyFiles/Downloads/report.txt
# Right-click any .py file in ChromeOS Files app:
# "Open with" → "Code" (VS Code) to edit it directlyRunning in the Background
python3 app.py & # Run in background
jobs # List background jobs
kill %1 # Stop background job 1
# Keep running after terminal close
nohup python3 app.py & # Output goes to nohup.out
disown # Detach from terminalHello World
#!/usr/bin/env python3
"""
hello.py — Your first Python script on a Chromebook.
Run with: python3 hello.py
"""
import sys
import platform
import os
from pathlib import Path
# f-strings — the modern Python string interpolation
name = "Chromebook"
year = __import__("datetime").date.today().year
print(f"Hello from {name}!")
print(f"Welcome to Python development in {year}.\n")
# Platform details
print("── System ───────────────────────────────")
print(f" Python : {sys.version.split()[0]}")
print(f" Platform : {platform.platform()}")
print(f" Machine : {platform.machine()}")
print(f" Node : {platform.node()}")
print(f" Home dir : {Path.home()}")
print(f" Shell : {os.environ.get('SHELL', 'unknown')}")
# List comprehension — a distinctly Pythonic pattern
tools = ["VS Code", "Jupyter", "IPython", "Black", "pytest"]
print("\n── Your Python tools ────────────────────")
for i, tool in enumerate(tools, 1):
print(f" {i}. {tool}")
# Dictionary
info = {
"OS" : "ChromeOS Linux (Crostini)",
"Distro" : platform.linux_distribution()[0] if hasattr(platform, 'linux_distribution') else "Debian",
"Python" : sys.version.split()[0],
"pip" : __import__("subprocess").run(
["pip3", "--version"], capture_output=True, text=True
).stdout.split()[1],
}
print("\n── Environment ──────────────────────────")
for key, val in info.items():
print(f" {key:<10}: {val}")
print("\nSetup complete — happy Python coding! 🐍")CLI Scripts & argparse
Python's argparse module makes it easy to write professional command-line tools — the kind that work just like standard Linux utilities.
#!/usr/bin/env python3
"""
sysinfo.py — Chromebook Linux system information reporter.
Run: python3 sysinfo.py
python3 sysinfo.py --json
python3 sysinfo.py --section memory
"""
import argparse
import json
import os
import platform
import sys
from pathlib import Path
def read_meminfo() -> dict:
"""Parse /proc/meminfo into a dict of {key: kB_value}."""
mem = {}
try:
with open("/proc/meminfo") as f:
for line in f:
parts = line.split()
if len(parts) >= 2:
mem[parts[0].rstrip(":")] = int(parts[1])
except FileNotFoundError:
pass
return mem
def read_cpu_count() -> int:
"""Count logical CPUs from /proc/cpuinfo."""
count = 0
try:
with open("/proc/cpuinfo") as f:
for line in f:
if line.startswith("processor"):
count += 1
except FileNotFoundError:
count = os.cpu_count() or 1
return count
def get_disk_info(path: str = None) -> dict:
"""Return disk usage for the given path (default: home dir)."""
target = path or str(Path.home())
stat = os.statvfs(target)
total = stat.f_blocks * stat.f_frsize
free = stat.f_bfree * stat.f_frsize
return {
"path" : target,
"total" : total,
"used" : total - free,
"free" : free,
"pct" : round(100 * (total - free) / max(total, 1)),
}
def collect_info() -> dict:
mem = read_meminfo()
disk = get_disk_info()
total_kb = mem.get("MemTotal", 0)
avail_kb = mem.get("MemAvailable", 0)
used_kb = total_kb - avail_kb
return {
"system": {
"hostname" : platform.node(),
"kernel" : platform.release(),
"arch" : platform.machine(),
"python" : sys.version.split()[0],
"pip" : _pip_version(),
},
"cpu": {
"count" : read_cpu_count(),
"model" : _cpu_model(),
},
"memory": {
"total_gb" : round(total_kb / 1_048_576, 1),
"used_gb" : round(used_kb / 1_048_576, 1),
"avail_gb" : round(avail_kb / 1_048_576, 1),
"pct_used" : round(100 * used_kb / max(total_kb, 1)),
},
"disk": {
"path" : disk["path"],
"total_gb" : round(disk["total"] / 1_073_741_824, 1),
"used_gb" : round(disk["used"] / 1_073_741_824, 1),
"free_gb" : round(disk["free"] / 1_073_741_824, 1),
"pct_used" : disk["pct"],
},
"env": {
"shell" : os.environ.get("SHELL", "?"),
"display" : os.environ.get("DISPLAY", "(not set)"),
"home" : str(Path.home()),
},
}
def _pip_version() -> str:
import subprocess
try:
r = subprocess.run(["pip3","--version"], capture_output=True, text=True)
return r.stdout.split()[1]
except Exception:
return "unknown"
def _cpu_model() -> str:
try:
with open("/proc/cpuinfo") as f:
for line in f:
if "model name" in line:
return line.split(":")[1].strip()
except Exception:
pass
return platform.processor() or "unknown"
def print_section(title: str, data: dict, indent: int = 2) -> None:
pad = " " * indent
print(f"\n{'─'*48}")
print(f" {title}")
print(f"{'─'*48}")
for key, val in data.items():
print(f"{pad}{key:<14}: {val}")
def main():
parser = argparse.ArgumentParser(
description="Chromebook Linux system information reporter",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Examples:\n"
" python3 sysinfo.py\n"
" python3 sysinfo.py --json\n"
" python3 sysinfo.py --section memory\n"
)
parser.add_argument("--json", action="store_true",
help="output as JSON")
parser.add_argument("--section", choices=["system","cpu","memory","disk","env"],
help="show only one section")
args = parser.parse_args()
info = collect_info()
if args.json:
print(json.dumps(info, indent=2))
return
sections = [args.section] if args.section else list(info.keys())
for sec in sections:
if sec in info:
print_section(sec.upper(), info[sec])
print()
if __name__ == "__main__":
main()File & Path Operations
Python's pathlib module provides an intuitive, object-oriented interface for working with files and directories — much cleaner than the old os.path approach.
#!/usr/bin/env python3
"""
files.py — File system analyzer using pathlib.
Usage: python3 files.py [directory]
python3 files.py ~
python3 files.py ~/projects --ext .py
"""
import argparse
import sys
from collections import Counter, defaultdict
from pathlib import Path
def analyze_directory(root: Path, include_hidden: bool = False,
ext_filter: str = None) -> dict:
"""Walk a directory tree and collect file statistics."""
stats = {
"total_files" : 0,
"total_size" : 0,
"by_extension" : Counter(),
"by_size" : defaultdict(int),
"largest" : [],
}
for path in root.rglob("*"):
# Skip hidden files unless requested
if not include_hidden and any(p.startswith(".") for p in path.parts):
continue
if not path.is_file():
continue
# Extension filter
if ext_filter and path.suffix.lower() != ext_filter.lower():
continue
size = path.stat().st_size
ext = path.suffix.lower() or "(none)"
stats["total_files"] += 1
stats["total_size"] += size
stats["by_extension"][ext] += 1
stats["largest"].append((size, str(path.relative_to(root))))
# Keep only the top 10 largest
stats["largest"] = sorted(stats["largest"], reverse=True)[:10]
return stats
def human_size(n_bytes: int) -> str:
"""Convert bytes to a human-readable string."""
for unit in ("B", "KB", "MB", "GB", "TB"):
if n_bytes < 1024:
return f"{n_bytes:.1f} {unit}"
n_bytes /= 1024
return f"{n_bytes:.1f} PB"
def main():
parser = argparse.ArgumentParser(description="Analyze a directory")
parser.add_argument("directory", nargs="?", default=str(Path.home()),
help="Directory to analyze (default: home)")
parser.add_argument("-a", "--all", action="store_true",
help="Include hidden files")
parser.add_argument("--ext", metavar="EXT",
help="Filter by extension, e.g. .py")
parser.add_argument("--top", type=int, default=15,
help="Show top N extensions (default: 15)")
args = parser.parse_args()
root = Path(args.directory).expanduser().resolve()
if not root.is_dir():
print(f"Error: '{root}' is not a directory.", file=sys.stderr)
sys.exit(1)
print(f"\nAnalyzing: {root}")
print("(this may take a moment for large directories...)\n")
stats = analyze_directory(root, args.all, args.ext)
# Summary
print(f"{'Files found:':<20} {stats['total_files']:,}")
print(f"{'Total size:':<20} {human_size(stats['total_size'])}")
# By extension
if stats["by_extension"]:
print(f"\n{'Extension':<14} {'Count':>7} {'Share':>7}")
print("─" * 32)
total = stats["total_files"]
for ext, count in stats["by_extension"].most_common(args.top):
bar = "█" * int(20 * count / max(total, 1))
print(f" {ext:<12} {count:>7} {100*count/total:>6.1f}% {bar}")
# Largest files
if stats["largest"]:
print(f"\nLargest files:")
for size, name in stats["largest"]:
print(f" {human_size(size):>10} {name}")
print()
if __name__ == "__main__":
main()Data Processing
CSV + dataclasses + statistics
#!/usr/bin/env python3
"""
csv_report.py — CSV sales data analyzer.
Demonstrates: csv, dataclasses, statistics, typing, f-strings
Usage: python3 csv_report.py (uses demo data)
python3 csv_report.py data.csv
"""
import csv
import sys
import statistics
from dataclasses import dataclass, field
from collections import defaultdict
from pathlib import Path
from typing import List
# ── Data model ─────────────────────────────────────────────────────────────
@dataclass
class SaleRecord:
product : str
category : str
units : int
price : float
@property
def revenue(self) -> float:
return self.units * self.price
# ── Demo data ───────────────────────────────────────────────────────────────
DEMO_CSV = """Product,Category,Units,Price
Widget Pro,Electronics,42,29.99
Gadget Lite,Electronics,87,14.99
Desk Lamp,Home,31,45.00
USB Hub,Electronics,124,19.99
Notebook,Stationery,200,5.99
Pen Set,Stationery,315,8.49
Monitor Stand,Home,28,79.99
Keyboard,Electronics,56,49.99
Mouse Pad,Electronics,93,12.99
Bookend Set,Home,44,24.99
"""
def load_records(source: str) -> List[SaleRecord]:
"""Load CSV data from a file path or the demo string."""
records = []
if source == "__demo__":
import io
reader = csv.DictReader(io.StringIO(DEMO_CSV))
else:
reader = csv.DictReader(open(source))
for row in reader:
try:
records.append(SaleRecord(
product = row["Product"],
category = row["Category"],
units = int(row["Units"]),
price = float(row["Price"]),
))
except (ValueError, KeyError):
continue # Skip malformed rows
return records
def print_table(records: List[SaleRecord]) -> None:
"""Print a formatted table of all records."""
fmt = f" {{:<22}} {{:<14}} {{:>6}} {{:>8}} {{:>10}}"
header = fmt.format("Product", "Category", "Units", "Price", "Revenue")
print(header)
print(" " + "─" * 64)
for r in sorted(records, key=lambda x: -x.revenue):
print(fmt.format(r.product, r.category, r.units,
f"${r.price:.2f}", f"${r.revenue:.2f}"))
total = sum(r.revenue for r in records)
print(" " + "─" * 64)
print(fmt.format("TOTAL", "", sum(r.units for r in records), "", f"${total:.2f}"))
def summarise_by_category(records: List[SaleRecord]) -> None:
"""Group records by category and print aggregates."""
groups: dict = defaultdict(list)
for r in records:
groups[r.category].append(r)
print("\nBy Category:")
for cat in sorted(groups, key=lambda c: -sum(r.revenue for r in groups[c])):
recs = groups[cat]
rev = sum(r.revenue for r in recs)
units = sum(r.units for r in recs)
print(f" {cat:<14} {len(recs):2d} products "
f"{units:5d} units ${rev:9.2f} revenue")
def main():
source = sys.argv[1] if len(sys.argv) > 1 else "__demo__"
if source == "__demo__":
print("(Using demo data — pass a CSV file as argument)\n")
elif not Path(source).exists():
print(f"File not found: {source}", file=sys.stderr)
sys.exit(1)
records = load_records(source)
if not records:
print("No valid records found.")
return
print(f"Records loaded: {len(records)}\n")
print_table(records)
summarise_by_category(records)
revenues = [r.revenue for r in records]
print(f"\nStatistics:")
print(f" Total revenue : ${sum(revenues):.2f}")
print(f" Mean per product: ${statistics.mean(revenues):.2f}")
print(f" Median : ${statistics.median(revenues):.2f}")
print(f" Stdev : ${statistics.stdev(revenues):.2f}")
best = max(records, key=lambda r: r.revenue)
print(f" Best product : {best.product} (${best.revenue:.2f})")
if __name__ == "__main__":
main()Classes, OOP & Type Hints
#!/usr/bin/env python3
"""
oop_demo.py — Object-oriented Python with type hints.
Run: python3 oop_demo.py
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional, List, Iterator
from abc import ABC, abstractmethod
import math
# ── Abstract base class ────────────────────────────────────────────────────
class Shape(ABC):
"""Abstract base — all shapes must implement area and perimeter."""
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
def describe(self) -> str:
return (f"{type(self).__name__}: "
f"area={self.area():.2f}, "
f"perimeter={self.perimeter():.2f}")
# ── Concrete shapes (dataclasses handle __init__, __repr__) ──────────────────
@dataclass
class Circle(Shape):
radius: float
def area(self) -> float: return math.pi * self.radius ** 2
def perimeter(self) -> float: return 2 * math.pi * self.radius
@dataclass
class Rectangle(Shape):
width: float
height: float
def area(self) -> float: return self.width * self.height
def perimeter(self) -> float: return 2 * (self.width + self.height)
@property
def is_square(self) -> bool:
return self.width == self.height
@dataclass
class Triangle(Shape):
a: float; b: float; c: float
def __post_init__(self):
if not (self.a + self.b > self.c and
self.b + self.c > self.a and
self.a + self.c > self.b):
raise ValueError("Invalid triangle sides")
def area(self) -> float:
s = self.perimeter() / 2
return math.sqrt(s * (s-self.a) * (s-self.b) * (s-self.c))
def perimeter(self) -> float: return self.a + self.b + self.c
# ── Container with dunder methods ───────────────────────────────────────────
class Canvas:
"""A collection of shapes with sorting and filtering."""
def __init__(self, name: str = "Canvas"):
self.name = name
self._shapes: List[Shape] = []
def add(self, *shapes: Shape) -> Canvas:
self._shapes.extend(shapes)
return self # Allows chaining: canvas.add(c).add(r)
def __len__(self) -> int: return len(self._shapes)
def __iter__(self) -> Iterator[Shape]: return iter(self._shapes)
def __getitem__(self, idx: int) -> Shape:
return self._shapes[idx]
@property
def total_area(self) -> float:
return sum(s.area() for s in self._shapes)
def by_type(self, cls: type) -> List[Shape]:
return [s for s in self._shapes if isinstance(s, cls)]
def sorted_by_area(self) -> List[Shape]:
return sorted(self._shapes, key=lambda s: s.area(), reverse=True)
def __repr__(self) -> str:
return f"Canvas({self.name!r}, {len(self)} shapes)"
# ── Context manager ──────────────────────────────────────────────────────────
class TempFile:
"""Creates a temp file, deletes it on __exit__."""
def __init__(self, path: str, content: str = ""):
self.path = path
self.content = content
def __enter__(self) -> TempFile:
with open(self.path, "w") as f:
f.write(self.content)
return self
def __exit__(self, *args) -> None:
import os
try: os.remove(self.path)
except FileNotFoundError: pass
# ── Demo ─────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
canvas = Canvas("Geometry Demo")
canvas.add(
Circle(5),
Circle(3),
Rectangle(10, 4),
Rectangle(6, 6),
Triangle(3, 4, 5),
)
print(f"{canvas}\n")
print("All shapes (sorted by area):")
for shape in canvas.sorted_by_area():
print(f" {shape.describe()}")
print(f"\nCircles only: {canvas.by_type(Circle)}")
print(f"Squares: {[r for r in canvas.by_type(Rectangle) if r.is_square]}")
print(f"Total area: {canvas.total_area:.2f}")
# Context manager demo
with TempFile("/tmp/demo.txt", "Hello from Python!\n") as tf:
content = open(tf.path).read()
print(f"\nTempFile content: {content.strip()!r}")
import os
print(f"TempFile deleted: {not os.path.exists('/tmp/demo.txt')}")GUI Development on Chromebook
Python GUI apps — Tkinter, GTK3, PyQt5 — all run as first-class ChromeOS windows via XWayland/Sommelier. No separate X11 server, no configuration: just run the script and the window appears alongside Chrome tabs.
DISPLAY environment variable is pre-set to :0 in every Terminal session. Python GUI apps just work.echo $DISPLAY # Should print :0 or :1
# Quick Tkinter test
python3 -c "
import tkinter as tk
win = tk.Tk()
win.title('ChromeOS GUI Test')
tk.Label(win, text='✓ GUI works!', font=('Helvetica',16,'bold'),
fg='#34c759').pack(pady=20)
tk.Button(win, text='Close', command=win.quit).pack()
win.mainloop()
"Adding Your App to the ChromeOS Launcher
mkdir -p ~/.local/share/applications
cat > ~/.local/share/applications/py-notes.desktop << 'EOF'
[Desktop Entry]
Name=Python Notes
Comment=Tkinter Notes Application
Exec=python3 /home/user/projects/gui/tk_notes.py
Icon=text-editor
Terminal=false
Type=Application
Categories=Utility;TextEditor;
EOF
update-desktop-database ~/.local/share/applications
# The app now appears in the ChromeOS launcherTkinter App — Chromebook Notes
A fully-featured Tkinter notes application. Notes are saved as text files in ~/notes/ — visible in the ChromeOS Files app. Demonstrates classes, menus, paned layout, keyboard shortcuts, and the dark custom theme.
#!/usr/bin/env python3
"""
tk_notes.py — Chromebook Notes (Tkinter)
Install: sudo apt install python3-tk
Run: python3 tk_notes.py
Notes are saved in ~/notes/ and are visible in the ChromeOS Files app.
"""
import glob
import os
import re
import time
import tkinter as tk
from tkinter import messagebox
from pathlib import Path
NOTES_DIR = Path.home() / "notes"
NOTES_DIR.mkdir(exist_ok=True)
class NotesApp:
# ── Colours & fonts (dark theme) ─────────────────────────────────────
BG = "#1e1e2e"
BG_SIDE = "#16161e"
BG_ENTRY = "#2a2a3e"
FG = "#e2d9c8"
FG_DIM = "#9b9380"
ACCENT = "#5bc7f0" # Chromebook blue
SEL_BG = "#2a3a5a"
FONT_UI = ("Helvetica", 11)
FONT_HDR = ("Helvetica", 14, "bold")
FONT_ED = ("Helvetica", 12)
FONT_SB = ("Helvetica", 9)
def __init__(self, root: tk.Tk):
self.root = root
self.current_note = ""
self.modified = False
self.root.title("Chromebook Notes")
self.root.geometry("800x580+60+40")
self.root.configure(bg=self.BG)
self.root.protocol("WM_DELETE_WINDOW", self.on_quit)
self._build_menu()
self._build_layout()
self._bind_shortcuts()
self._load_note_list()
# ── Menu bar ──────────────────────────────────────────────────────────
def _build_menu(self):
opts = dict(bg=self.BG_SIDE, fg=self.FG, tearoff=0,
activebackground=self.SEL_BG, activeforeground=self.FG)
menubar = tk.Menu(self.root, bg=self.BG_SIDE, fg=self.FG)
file_m = tk.Menu(menubar, **opts)
file_m.add_command(label="New Note Ctrl+N", command=self.cmd_new)
file_m.add_command(label="Save Ctrl+S", command=self.cmd_save)
file_m.add_separator()
file_m.add_command(label="Quit Ctrl+Q", command=self.on_quit)
edit_m = tk.Menu(menubar, **opts)
for label, event in [
("Undo Ctrl+Z", "<>"),
("Redo Ctrl+Y", "<>"),
None,
("Cut Ctrl+X", "<>"),
("Copy Ctrl+C", "<>"),
("Paste Ctrl+V", "<>"),
]:
if label is None:
edit_m.add_separator()
else:
edit_m.add_command(
label=label,
command=lambda e=event: self.editor.event_generate(e)
)
menubar.add_cascade(label="File", menu=file_m)
menubar.add_cascade(label="Edit", menu=edit_m)
self.root.config(menu=menubar)
# ── Layout: sidebar + editor ──────────────────────────────────────────
def _build_layout(self):
main = tk.Frame(self.root, bg=self.BG)
main.pack(fill="both", expand=True)
# Sidebar
side = tk.Frame(main, bg=self.BG_SIDE, width=215)
side.pack(side="left", fill="y")
side.pack_propagate(False)
tk.Label(side, text="MY NOTES", bg=self.BG_SIDE, fg=self.ACCENT,
font=self.FONT_SB, anchor="w").pack(
fill="x", padx=10, pady=(12, 4))
# Note list
lb_wrap = tk.Frame(side, bg=self.BG_SIDE)
lb_wrap.pack(fill="both", expand=True)
self.listbox = tk.Listbox(
lb_wrap, bg=self.BG_SIDE, fg=self.FG,
selectbackground=self.SEL_BG, selectforeground=self.ACCENT,
font=self.FONT_UI, bd=0, relief="flat", activestyle="none",
)
self.listbox.pack(side="left", fill="both", expand=True)
sb = tk.Scrollbar(lb_wrap, command=self.listbox.yview, width=8)
sb.pack(side="right", fill="y")
self.listbox.configure(yscrollcommand=sb.set)
self.listbox.bind("<>", self._on_list_select)
# Sidebar buttons
btn_row = tk.Frame(side, bg=self.BG_SIDE)
btn_row.pack(fill="x", padx=8, pady=6)
self._btn(btn_row, "+ New", bg="#2a4a8a", fg="white",
cmd=self.cmd_new).pack(side="left", padx=2)
self._btn(btn_row, "⌫ Delete", bg="#4a1a1a", fg="#e08080",
cmd=self.cmd_delete).pack(side="left", padx=2)
# Editor frame
ed_frame = tk.Frame(main, bg=self.BG)
ed_frame.pack(side="left", fill="both", expand=True)
self.title_var = tk.StringVar()
tk.Entry(
ed_frame, textvariable=self.title_var,
font=self.FONT_HDR, bg=self.BG_ENTRY, fg=self.FG,
insertbackground="white", relief="flat", bd=4,
).pack(fill="x", padx=8, pady=6)
txt_wrap = tk.Frame(ed_frame, bg=self.BG)
txt_wrap.pack(fill="both", expand=True, padx=8)
self.editor = tk.Text(
txt_wrap, font=self.FONT_ED, bg=self.BG, fg=self.FG,
insertbackground="white", selectbackground="#264f78",
relief="flat", padx=4, pady=4, wrap="word", undo=True,
)
self.editor.pack(side="left", fill="both", expand=True)
ed_sb = tk.Scrollbar(txt_wrap, command=self.editor.yview, width=8)
ed_sb.pack(side="right", fill="y")
self.editor.configure(yscrollcommand=ed_sb.set)
self.editor.bind("<>", self._on_editor_modified)
# Toolbar below editor
tool = tk.Frame(ed_frame, bg="#16161e")
tool.pack(fill="x", pady=4)
self._btn(tool, "Save Ctrl+S", bg="#2a4a2a", fg="#6fcf97",
cmd=self.cmd_save, padx=10, pady=4).pack(
side="left", padx=4)
# Status bar
self.status_var = tk.StringVar(value="Ready")
status = tk.Frame(self.root, bg=self.ACCENT, height=22)
status.pack(fill="x", side="bottom")
tk.Label(status, textvariable=self.status_var, bg=self.ACCENT,
fg="#0a1a2a", font=("Helvetica", 10), anchor="w",
).pack(side="left", padx=10)
tk.Label(status, text=f"~/notes/", bg=self.ACCENT,
fg="#0a1a2a", font=("Helvetica", 10),
).pack(side="right", padx=10)
def _btn(self, parent, text, bg, fg, cmd, padx=8, pady=3):
return tk.Button(parent, text=text, bg=bg, fg=fg, relief="flat",
font=("Helvetica", 10), padx=padx, pady=pady,
command=cmd, activebackground=bg, activeforeground=fg)
# ── Keyboard shortcuts ────────────────────────────────────────────────
def _bind_shortcuts(self):
self.root.bind("", lambda _: self.cmd_new())
self.root.bind("", lambda _: self.cmd_save())
self.root.bind("", lambda _: self.on_quit())
# ── Note list management ──────────────────────────────────────────────
def _load_note_list(self):
self.listbox.delete(0, "end")
for f in sorted(NOTES_DIR.glob("*.txt")):
self.listbox.insert("end", f.stem)
def _on_list_select(self, _event=None):
sel = self.listbox.curselection()
if not sel:
return
self._maybe_save_current()
self._load_note(self.listbox.get(sel[0]))
def _load_note(self, name: str):
path = NOTES_DIR / f"{name}.txt"
if not path.exists():
return
content = path.read_text()
title = content.splitlines()[0].lstrip("# ") if content else name
self.title_var.set(title)
self.editor.delete("1.0", "end")
self.editor.insert("1.0", content)
self.editor.edit_modified(False)
self.current_note = name
self.modified = False
self.root.title(f"Chromebook Notes — {name}")
self.status_var.set(f"Loaded: {name}")
def _on_editor_modified(self, _event=None):
if self.editor.edit_modified():
self.modified = True
self.root.title("Chromebook Notes •")
def _maybe_save_current(self):
if self.modified:
if messagebox.askyesno("Save?", "Save current note first?",
parent=self.root):
self.cmd_save()
# ── Commands ──────────────────────────────────────────────────────────
def cmd_new(self):
self._maybe_save_current()
self.title_var.set("New Note")
self.editor.delete("1.0", "end")
self.editor.insert("1.0", "# New Note\n\n")
self.editor.edit_modified(False)
self.current_note = f"note_{int(time.time())}"
self.modified = False
self.root.title("Chromebook Notes — new")
self.status_var.set("New note")
def cmd_save(self):
name = re.sub(r"[^\w\s-]", "", self.title_var.get())
name = re.sub(r"\s+", "_", name).lower().strip()
name = name or self.current_note or f"untitled_{int(time.time())}"
content = self.editor.get("1.0", "end")
(NOTES_DIR / f"{name}.txt").write_text(content)
self.editor.edit_modified(False)
self.current_note = name
self.modified = False
self.root.title(f"Chromebook Notes — {name}")
self.status_var.set(f"Saved: {name}.txt")
self._load_note_list()
def cmd_delete(self):
sel = self.listbox.curselection()
if not sel:
return
name = self.listbox.get(sel[0])
if messagebox.askyesno("Delete?", f"Delete '{name}'?",
parent=self.root):
(NOTES_DIR / f"{name}.txt").unlink(missing_ok=True)
self.editor.delete("1.0", "end")
self.title_var.set("")
self.current_note = ""
self.modified = False
self.root.title("Chromebook Notes")
self._load_note_list()
self.status_var.set(f"Deleted: {name}")
def on_quit(self):
if self.modified:
self.cmd_save()
self.root.quit()
if __name__ == "__main__":
root = tk.Tk()
NotesApp(root)
root.mainloop() GTK3 App — System Monitor
A live-updating system monitor using PyGObject/GTK3. Reads /proc data every second via GLib.timeout_add() and renders progress bars that change colour as resources fill up.
#!/usr/bin/env python3
"""
gtk3_sysmon.py — Live System Monitor (GTK3 / PyGObject)
Install: sudo apt install python3-gi gir1.2-gtk-3.0
Run: python3 gtk3_sysmon.py
"""
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk
import os
import subprocess
from pathlib import Path
# ── System data readers ────────────────────────────────────────────────────
_prev_cpu = [0] * 8
def cpu_percent() -> int:
global _prev_cpu
try:
line = open("/proc/stat").readline()
fields = list(map(int, line.split()[1:]))
idle = fields[3] + fields[4]
total = sum(fields)
d_idle = idle - _prev_cpu[3] - _prev_cpu[4]
d_total = total - sum(_prev_cpu)
_prev_cpu = fields[:8]
return 0 if d_total == 0 else max(0, min(100, int(100 - 100 * d_idle / d_total)))
except Exception:
return 0
def mem_info() -> tuple[int, int]:
mem = {}
try:
for line in open("/proc/meminfo"):
k, *v = line.split()
if v: mem[k.rstrip(":")] = int(v[0])
except Exception:
pass
total = mem.get("MemTotal", 1)
avail = mem.get("MemAvailable", 0)
return total, total - avail
def disk_info(path: str = None) -> tuple[int, int]:
try:
st = os.statvfs(path or str(Path.home()))
tot = st.f_blocks * st.f_frsize
free = st.f_bfree * st.f_frsize
return tot, tot - free
except Exception:
return 1, 0
# ── GTK3 Application ────────────────────────────────────────────────────────
class SysMonApp(Gtk.Application):
def __init__(self):
super().__init__(application_id="com.chromebook.sysmon")
self.connect("activate", self.on_activate)
def on_activate(self, app):
win = SysMonWindow(application=self)
win.show_all()
class SysMonWindow(Gtk.ApplicationWindow):
CSS = b"""
window { background-color: #09090f; }
frame { background-color: #0f0f1c;
border-radius: 8px; border: 1px solid #1e1e30; }
.title-lbl { color: #4B8BBE; font-size: 20px; font-weight: bold; }
.section-lbl { color: #9b9380; font-size: 12px; }
.val-ok { color: #6fcf97; font-size: 15px; font-weight: bold; }
.val-warn { color: #f0a030; font-size: 15px; font-weight: bold; }
.val-high { color: #eb5757; font-size: 15px; font-weight: bold; }
.detail-lbl { color: #5a5448; font-size: 11px; }
progressbar trough { min-height: 14px; border-radius: 7px;
background-color: #1e1e30; }
progressbar.ok progress { border-radius: 7px; background-color: #6fcf97; }
progressbar.warn progress { border-radius: 7px; background-color: #f0a030; }
progressbar.high progress { border-radius: 7px; background-color: #eb5757; }
separator { background-color: #1e1e30; min-height: 1px; }
"""
def __init__(self, **kwargs):
super().__init__(title="System Monitor", **kwargs)
self.set_default_size(460, 380)
self.set_resizable(False)
# Apply CSS
provider = Gtk.CssProvider()
provider.load_from_data(self.CSS)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(), provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
self._bars = {}
self._vals = {}
self._detail = {}
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
root.set_margin_start(20); root.set_margin_end(20)
root.set_margin_top(18); root.set_margin_bottom(18)
self.add(root)
# Title
title = Gtk.Label(label="")
title.set_markup(
''
'Chromebook System Monitor'
)
title.set_margin_bottom(16)
root.pack_start(title, False, False, 0)
# Cards for CPU, Memory, Disk
for metric_id, label_text in [
("cpu", "CPU"),
("mem", "Memory"),
("disk", "Disk"),
]:
card = self._make_card(metric_id, label_text)
card.set_margin_bottom(10)
root.pack_start(card, False, False, 0)
# Updated label
self._updated = Gtk.Label(label="")
self._updated.set_halign(Gtk.Align.END)
self._updated.set_margin_top(6)
root.pack_end(self._updated, False, False, 0)
# Start 1-second timer
self._update()
GLib.timeout_add(1000, self._update)
def _make_card(self, mid: str, label_text: str) -> Gtk.Frame:
frame = Gtk.Frame()
frame.set_shadow_type(Gtk.ShadowType.NONE)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
vbox.set_margin_start(16); vbox.set_margin_end(16)
vbox.set_margin_top(12); vbox.set_margin_bottom(12)
frame.add(vbox)
# Header: section label + value
hdr = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
lbl = Gtk.Label(label=label_text)
lbl.get_style_context().add_class("section-lbl")
lbl.set_halign(Gtk.Align.START)
hdr.pack_start(lbl, True, True, 0)
val = Gtk.Label(label="— %")
val.get_style_context().add_class("val-ok")
val.set_halign(Gtk.Align.END)
self._vals[mid] = val
hdr.pack_end(val, False, False, 0)
vbox.pack_start(hdr, False, False, 0)
# Progress bar
bar = Gtk.ProgressBar()
bar.set_fraction(0)
bar.get_style_context().add_class("ok")
self._bars[mid] = bar
vbox.pack_start(bar, False, False, 0)
# Detail text
det = Gtk.Label(label="")
det.get_style_context().add_class("detail-lbl")
det.set_halign(Gtk.Align.START)
det.set_margin_top(2)
self._detail[mid] = det
vbox.pack_start(det, False, False, 0)
return frame
def _set_metric(self, mid: str, pct: int, detail: str):
cls = "high" if pct > 80 else "warn" if pct > 55 else "ok"
col = "#eb5757" if pct > 80 else "#f0a030" if pct > 55 else "#6fcf97"
bar = self._bars[mid]
ctx = bar.get_style_context()
for c in ("ok", "warn", "high"):
ctx.remove_class(c)
ctx.add_class(cls)
bar.set_fraction(pct / 100)
val = self._vals[mid]
for c in ("val-ok", "val-warn", "val-high"):
val.get_style_context().remove_class(c)
val.get_style_context().add_class(f"val-{cls}")
val.set_text(f"{pct}%")
self._detail[mid].set_markup(
f'{detail}'
)
def _update(self) -> bool:
import time
# CPU
self._set_metric("cpu", cpu_percent(), "Utilisation across all cores")
# Memory
tot_kb, used_kb = mem_info()
pct = int(100 * used_kb / max(tot_kb, 1))
self._set_metric(
"mem", pct,
f"{used_kb/1_048_576:.1f} GB used of {tot_kb/1_048_576:.1f} GB"
)
# Disk
tot_b, used_b = disk_info()
pct = int(100 * used_b / max(tot_b, 1))
self._set_metric(
"disk", pct,
f"{used_b/1_073_741_824:.1f} GB used of {tot_b/1_073_741_824:.1f} GB"
)
# Timestamp
self._updated.set_markup(
f''
f'Updated: {time.strftime("%H:%M:%S")}'
)
return True # Keep the timer running
if __name__ == "__main__":
SysMonApp().run()Flask Web App — Notes Service
A complete Flask notes web application. Run it in the Linux terminal and access it from Chrome at http://localhost:5000 — no port forwarding needed on a Chromebook.
pip3 install flask --user
python3 flask_notes.py
# Open Chrome → http://localhost:5000#!/usr/bin/env python3
"""
flask_notes.py — Notes web app (Flask + inline templates)
Install: pip3 install flask
Run: python3 flask_notes.py
Visit: http://localhost:5000
Notes stored in ~/notes/ (shared with the Tkinter app).
"""
import re
from pathlib import Path
from flask import Flask, render_template_string, request, redirect, url_for
app = Flask(__name__)
NOTES = Path.home() / "notes"
NOTES.mkdir(exist_ok=True)
# ── Helpers ────────────────────────────────────────────────────────────────
def all_notes() -> list[dict]:
notes = []
for f in sorted(NOTES.glob("*.txt")):
preview = ""
try:
first_line = f.read_text().splitlines()[0]
preview = first_line.lstrip("# ").strip()
except Exception:
pass
notes.append({"name": f.stem, "preview": preview})
return notes
def safe_name(raw: str) -> str:
name = re.sub(r"[^\w\s-]", "", raw)
name = re.sub(r"\s+", "_", name).lower().strip()
return name or f"note_{Path.cwd().stat().st_mtime_ns}"
# ── Routes ──────────────────────────────────────────────────────────────────
@app.route("/")
def index():
return render_template_string(LAYOUT, notes=all_notes(),
title="Notes", body=INDEX_BODY,
note_name="", note_body="")
@app.route("/note/")
def view_note(name: str):
name = safe_name(name)
path = NOTES / f"{name}.txt"
body = path.read_text() if path.exists() else ""
return render_template_string(LAYOUT, notes=all_notes(),
title=name, body=NOTE_BODY,
note_name=name, note_body=body)
@app.route("/new")
def new_note():
return render_template_string(LAYOUT, notes=all_notes(),
title="New Note", body=NOTE_BODY,
note_name="new_note",
note_body="# New Note\n\n")
@app.route("/save", methods=["POST"])
def save_note():
raw_name = request.form.get("name", "untitled")
name = safe_name(raw_name)
content = request.form.get("body", "")
(NOTES / f"{name}.txt").write_text(content)
return redirect(url_for("view_note", name=name))
@app.route("/delete/", methods=["POST"])
def delete_note(name: str):
(NOTES / f"{safe_name(name)}.txt").unlink(missing_ok=True)
return redirect(url_for("index"))
# ── Templates (Jinja2 inline strings) ──────────────────────────────────────
LAYOUT = """
{{ title }} — Chromebook Notes
{{ body | safe }}
"""
INDEX_BODY = """
Chromebook Notes
{{ notes | length }} note(s) stored in ~/notes/.
{% if not notes %}
Create your first note →
{% endif %}
"""
NOTE_BODY = """
"""
if __name__ == "__main__":
print("Starting Flask Notes on http://localhost:5000")
print("Open Chrome and visit: http://localhost:5000")
app.run(debug=True, port=5000) HTTP Clients & APIs
#!/usr/bin/env python3
"""
web_fetch.py — HTTP client examples with requests.
Install: pip3 install requests
Run: python3 web_fetch.py
"""
import json
from pathlib import Path
import requests
# Reuse a session for connection pooling
session = requests.Session()
session.headers["User-Agent"] = "PythonChromebook/1.0"
# ── GET: plain text ────────────────────────────────────────────────────────
print("=== GET: JSON API ===")
resp = session.get("https://httpbin.org/get", timeout=10)
resp.raise_for_status() # Raises on 4xx / 5xx
data = resp.json() # Parse JSON response
print(f" Status : {resp.status_code}")
print(f" Origin : {data.get('origin')}")
print(f" URL : {data.get('url')}")
# ── GET: GitHub REST API ───────────────────────────────────────────────────
print("\n=== GET: GitHub API ===")
resp = session.get(
"https://api.github.com/repos/python/cpython",
headers={"Accept": "application/vnd.github.v3+json"},
timeout=10,
)
if resp.ok:
repo = resp.json()
print(f" Repo : {repo['full_name']}")
print(f" Stars : {repo['stargazers_count']:,}")
print(f" Lang : {repo['language']}")
print(f" Desc : {repo['description'][:60]}…")
# ── POST: send JSON data ───────────────────────────────────────────────────
print("\n=== POST: JSON body ===")
payload = {"name": "Chromebook", "lang": "Python", "year": 2025}
resp = session.post("https://httpbin.org/post", json=payload, timeout=10)
if resp.ok:
body = resp.json()
print(f" Echoed : {body.get('json')}")
# ── Error handling ─────────────────────────────────────────────────────────
print("\n=== Error handling ===")
try:
session.get("https://httpbin.org/status/404", timeout=10).raise_for_status()
except requests.HTTPError as e:
print(f" HTTP error caught: {e.response.status_code}")
except requests.ConnectionError:
print(" Connection error — no internet?")
except requests.Timeout:
print(" Request timed out")
# ── Download a file ────────────────────────────────────────────────────────
print("\n=== Streaming download ===")
url = "https://www.python.org/static/img/python-logo.png"
dest = Path("/tmp/python-logo.png")
try:
with session.get(url, stream=True, timeout=10) as r:
r.raise_for_status()
dest.write_bytes(r.content)
print(f" Downloaded {dest.stat().st_size:,} bytes → {dest}")
except Exception as e:
print(f" Skipped: {e}")Jupyter Notebook on Chromebook
Jupyter Notebook is particularly well-suited to Chromebook: you run the server in the Linux container and open it in the Chrome browser — no extra software needed. The browser-based interface makes data exploration and visualisation feel native.
pip3 install --user jupyterlab jupyter notebook
# Verify
jupyter --versionjupyter notebook # Classic interface (http://localhost:8888)
jupyter lab # Modern JupyterLab (recommended)
# The terminal shows a URL like:
# http://localhost:8888/lab?token=abc123...
# Copy it into Chrome. Token is shown only on first start.jupyter notebook --no-browser --NotebookApp.token=''
# Or configure permanently:
jupyter notebook --generate-config
# Edit ~/.jupyter/jupyter_notebook_config.py:
# c.NotebookApp.token = ''
# c.NotebookApp.open_browser = FalseA Sample Notebook (equivalent Python cells)
pandas & NumPy
#!/usr/bin/env python3
"""
data_analysis.py — pandas and NumPy essentials.
Install: pip3 install pandas numpy
Run: python3 data_analysis.py
"""
import numpy as np
import pandas as pd
from pathlib import Path
# ══ NUMPY ═══════════════════════════════════════════════════════════════════
print("═" * 50)
print(" NumPy")
print("═" * 50)
# Array creation and operations
arr = np.array([1, 4, 9, 16, 25, 36, 49, 64, 81, 100])
print(f"\nArray: {arr}")
print(f"Sqrt: {np.sqrt(arr)}")
print(f"Mean: {np.mean(arr):.2f}")
print(f"Std dev: {np.std(arr):.2f}")
# 2-D matrix
matrix = np.arange(1, 10).reshape(3, 3)
print(f"\nMatrix:\n{matrix}")
print(f"Transpose:\n{matrix.T}")
print(f"Row sums: {matrix.sum(axis=1)}")
print(f"Col means: {matrix.mean(axis=0)}")
# Boolean indexing
nums = np.random.default_rng(42).integers(0, 100, 20)
print(f"\n20 random ints: {nums}")
print(f"Values > 50: {nums[nums > 50]}")
print(f"Count > 50: {(nums > 50).sum()}")
# ══ PANDAS ═══════════════════════════════════════════════════════════════════
print("\n" + "═" * 50)
print(" pandas")
print("═" * 50)
# Build a DataFrame from a dictionary
df = pd.DataFrame({
"name" : ["Alice", "Bob", "Carol", "Dave", "Eve"],
"dept" : ["Eng", "HR", "Eng", "Mkt", "Eng"],
"salary" : [95000, 68000, 102000, 75000, 89000],
"years" : [5, 3, 8, 2, 6],
})
print(f"\nShape: {df.shape}")
print(f"\n{df.to_string(index=False)}")
# Selection and filtering
print("\nEngineering team:")
eng = df[df["dept"] == "Eng"]
print(eng[["name","salary","years"]].to_string(index=False))
# Aggregation
print("\nSalary by department:")
print(df.groupby("dept")["salary"].agg(["mean","min","max","count"])
.rename(columns={"mean":"avg"})
.round(0).to_string())
# Adding computed columns
df["seniority"] = pd.cut(df["years"],
bins=[0, 2, 5, 100],
labels=["junior", "mid", "senior"]
)
print(f"\nWith seniority:\n{df[['name','seniority']].to_string(index=False)}")
# Sorting
top3 = df.nlargest(3, "salary")[["name","dept","salary"]]
print(f"\nTop 3 earners:\n{top3.to_string(index=False)}")
# Save and reload
csv_path = Path("/tmp/employees.csv")
df.to_csv(csv_path, index=False)
reloaded = pd.read_csv(csv_path)
print(f"\nSaved {len(reloaded)} rows to {csv_path}")
csv_path.unlink() # Clean upmatplotlib — Plots & Charts
On a Chromebook, matplotlib plots open as ChromeOS windows via XWayland — just like any other GUI app. You can also save them directly to PNG/SVG/PDF files.
#!/usr/bin/env python3
"""
plots.py — matplotlib chart gallery.
Install: pip3 install matplotlib numpy
Run: python3 plots.py (shows plots as ChromeOS windows)
python3 plots.py --save (saves PNGs, no window)
"""
import sys
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
SAVE_ONLY = "--save" in sys.argv
if SAVE_ONLY:
matplotlib.use("Agg") # Non-interactive backend for saving only
# Use a dark style for the Chromebook dark-themed desktop
plt.style.use("dark_background")
def demo_plots():
fig = plt.figure(figsize=(14, 10), facecolor="#09090f")
fig.suptitle("matplotlib on Chromebook",
color="#e2d9c8", fontsize=16, fontweight="bold", y=0.98)
gs = GridSpec(2, 3, figure=fig, hspace=0.45, wspace=0.35)
# ── Line chart ────────────────────────────────────────────────────────
ax1 = fig.add_subplot(gs[0, 0])
x = np.linspace(0, 4 * np.pi, 300)
ax1.plot(x, np.sin(x), color="#4B8BBE", linewidth=2, label="sin")
ax1.plot(x, np.cos(x), color="#FFD43B", linewidth=2, label="cos")
ax1.axhline(0, color="#3a3a4a", linewidth=0.8, linestyle="--")
ax1.legend(fontsize=9); ax1.set_title("Line Chart", color="#e2d9c8")
# ── Bar chart ──────────────────────────────────────────────────────────
ax2 = fig.add_subplot(gs[0, 1])
langs = ["Python", "JS", "Rust", "Go", "Java"]
scores = [88, 76, 72, 68, 60]
colours = ["#4B8BBE", "#f0a030", "#6fcf97", "#4dd0c8", "#eb5757"]
bars = ax2.bar(langs, scores, color=colours, edgecolor="none")
for bar, val in zip(bars, scores):
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
str(val), ha="center", va="bottom", fontsize=9, color="white")
ax2.set_ylim(0, 100); ax2.set_title("Bar Chart", color="#e2d9c8")
# ── Scatter plot ───────────────────────────────────────────────────────
ax3 = fig.add_subplot(gs[0, 2])
rng = np.random.default_rng(42)
n = 120
x_s = rng.standard_normal(n)
y_s = x_s * 0.8 + rng.standard_normal(n) * 0.5
cols = rng.uniform(0, 1, n)
sc = ax3.scatter(x_s, y_s, c=cols, cmap="plasma", alpha=0.8, s=30)
fig.colorbar(sc, ax=ax3, label="Random value")
ax3.set_title("Scatter Plot", color="#e2d9c8")
# ── Histogram ─────────────────────────────────────────────────────────
ax4 = fig.add_subplot(gs[1, 0])
data = rng.normal(100, 15, 1000)
ax4.hist(data, bins=30, color="#4B8BBE", edgecolor="#09090f", alpha=0.85)
ax4.axvline(data.mean(), color="#FFD43B", linestyle="--",
linewidth=2, label=f"Mean={data.mean():.1f}")
ax4.legend(fontsize=9); ax4.set_title("Histogram", color="#e2d9c8")
# ── Pie chart ──────────────────────────────────────────────────────────
ax5 = fig.add_subplot(gs[1, 1])
sizes = [35, 25, 20, 12, 8]
labels = ["Python", "JS", "Java", "C++", "Other"]
explode= [0.05] * len(sizes)
ax5.pie(sizes, labels=labels, autopct="%1.0f%%",
colors=colours, explode=explode,
textprops={"color": "#e2d9c8", "fontsize": 9})
ax5.set_title("Pie Chart", color="#e2d9c8")
# ── Area chart ─────────────────────────────────────────────────────────
ax6 = fig.add_subplot(gs[1, 2])
months = range(1, 13)
sales = [80, 90, 105, 120, 115, 130, 125, 118, 110, 100, 95, 150]
costs = [60, 65, 72, 80, 78, 88, 84, 80, 76, 70, 68, 100]
ax6.fill_between(months, sales, alpha=0.4, color="#4B8BBE", label="Sales")
ax6.fill_between(months, costs, alpha=0.4, color="#eb5757", label="Costs")
ax6.plot(months, sales, color="#4B8BBE", linewidth=2)
ax6.plot(months, costs, color="#eb5757", linewidth=2)
ax6.legend(fontsize=9); ax6.set_title("Area Chart", color="#e2d9c8")
ax6.set_xticks(months)
ax6.set_xticklabels(["J","F","M","A","M","J","J","A","S","O","N","D"])
return fig
fig = demo_plots()
if SAVE_ONLY:
path = "/tmp/chromebook_charts.png"
fig.savefig(path, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor())
print(f"Saved to {path}")
else:
plt.show() # Opens as a ChromeOS window via XWaylandTermux — Python via Android
Termux gives you Python in an Android terminal on Chromebooks that have the Play Store but lack the Linux container. CLI scripts and web servers work; GUI toolkits do not.
plt.show() will fail. Use plt.savefig() instead, and open the saved image in Chrome.# Install Termux from F-Droid (not Play Store — outdated version)
# https://f-droid.org/packages/com.termux/
pkg update && pkg upgrade -y
# Python is a single command
pkg install python
# Verify
python --version # Python 3.12.x (no '3' suffix in Termux)
pip --version
# Install packages
pip install flask requests pandas numpy matplotlib
# Access ChromeOS files
termux-setup-storage # Enables ~/storage/ → Android shared folders
ls ~/storage/downloads/
# Save a matplotlib plot to Downloads (viewable from Chrome)
python -c "
import matplotlib
matplotlib.use('Agg') # Non-GUI backend — REQUIRED in Termux
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
plt.plot(x, np.sin(x))
plt.savefig('/data/data/com.termux/files/home/storage/downloads/plot.png')
print('Plot saved to Downloads')
"Flask in Termux → Access from Chrome
# Run the Flask app in Termux
python flask_notes.py # Starts on localhost:5000
# Open Chrome on the Chromebook → http://localhost:5000
# (The Android localhost is the same as the Chromebook browser's localhost)Cloud IDEs & Notebooks
| Service | Best for | Python | Free tier |
|---|---|---|---|
| Google Colab | Data science, ML — GPU access free | 3.10 | Unlimited (CPU), limited GPU |
| Replit | General Python, web apps, sharing | 3.10+ | Yes, limited RAM |
| GitHub Codespaces | Full dev environment with git | 3.11+ | 60h/month |
| Gitpod | Pre-configured workspace from repo | 3.11+ | 50h/month |
| Kaggle Notebooks | Data science, ML competitions | 3.10 | 30h/week GPU |
# In a Google Colab notebook cell:
# Install extra packages
!pip install yfinance --quiet
import pandas as pd
import matplotlib.pyplot as plt
# Mount Google Drive (Colab-specific)
from google.colab import drive
drive.mount('/content/drive')
# Download data
import yfinance as yf
aapl = yf.download('AAPL', start='2023-01-01', progress=False)
aapl['Close'].plot(figsize=(12,4), title='Apple Stock 2023')
plt.savefig('/content/drive/MyDrive/aapl_chart.png')
plt.show()Tips & Best Practices
Python-Specific Tips for Chromebook
- Always use virtual environments for projects —
python3 -m venv .venvprevents dependency conflicts. - Use
pip3notpipwhen outside a venv to ensure you're installing for Python 3. - Black formatter — run
black .before committing. Consistent formatting is effortless with Black. - Type hints — add them from the start.
mypycatches bugs before they happen. - Pathlib over os.path —
Path.home() / "notes"is cleaner thanos.path.join(os.path.expanduser("~"), "notes"). - f-strings over format() —
f"Hello {name}!"is faster and more readable.
Chromebook Integration Tips
- Pin the Terminal to the shelf and set your default directory to
~/projects: addcd ~/projectsto your~/.bashrc. - Open VS Code from ChromeOS Files: right-click any
.pyfile → Open with → Code. - Drag files from ChromeOS Downloads directly into the terminal window to paste the full path.
- Jupyter Notebook: bookmark
http://localhost:8888/labin Chrome for one-click access. - Set up a shell alias:
alias jl='jupyter lab --no-browser'in~/.bashrc.
Backup Your Python Environment
# Export ALL globally installed pip packages
pip3 freeze > ~/pip_packages.txt
# Per-project (inside activated venv):
pip freeze > requirements.txt
# Copy projects to ChromeOS Downloads for backup
cp -r ~/projects /mnt/chromeos/MyFiles/Downloads/python_backup_$(date +%Y%m%d)
# Restore on a new Chromebook:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtQuick Cheat Sheet
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv python3-dev \
python3-tk python3-gi gir1.2-gtk-3.0 build-essential wget code
pip3 install --user black pylint mypy pytest ipython jupyterlab \
flask requests httpx pandas numpy matplotlib seaborn scikit-learn
code --install-extension ms-python.python ms-python.pylance \
ms-toolsai.jupyter ms-python.black-formattermkdir ~/projects/my_project && cd ~/projects/my_project
python3 -m venv .venv
source .venv/bin/activate
pip install flask # or pandas, or whatever you need
code . # VS Code auto-selects the venvsource .venv/bin/activate # Activate venv for this project
python3 script.py # Run
python3 -m pytest # Test
black . # Format
mypy script.py # Type check
jupyter lab # Data science
python3 app.py # Web app → localhost:5000 in Chrome| Goal | Command / Action |
|---|---|
| Enable Linux | Settings → Advanced → Developers → Linux |
| Open terminal | Launcher → Terminal |
| Python REPL | python3 or ipython |
| Jupyter in Chrome | jupyter lab → Chrome → localhost:8888 |
| Flask app in Chrome | python3 app.py → Chrome → localhost:5000 |
| GUI app as ChromeOS window | python3 tk_notes.py |
| Format code | black . |
| Type check | mypy script.py |
| Share project files | Files app → Linux files → drag to Downloads |
| Increase Linux disk | Settings → Developers → Linux → Disk size |