Thực hành: biến script Python thành lệnh mà đội khách tự cài và tự chạy bằng Typer
Script chạy được trên laptop của bạn vẫn chưa phải là thứ bàn giao được. Nó chỉ thành công cụ khi đội khách gõ một lệnh trên máy họ và chạy được ngay.

Tóm tắt nhanh
- Typer dùng type hint chuẩn của Python để tạo CLI có sẵn help và auto-completion cho Bash, Zsh, Fish, PowerShell.
- Lệnh được khai báo trong [project.scripts] và trỏ tới đối tượng app. Bảng [build-system] luôn phải có.
- pipx và uv tool install cài mỗi công cụ vào môi trường riêng, còn uvx chạy tool trong môi trường tạm, nên đội khách không vướng xung đột phụ thuộc.
- 1Tạo packageuv init --package, giữ nguyên bảng [build-system] trong pyproject.toml
- 2Viết lệnh bằng TyperKhai báo tham số bằng type hint, có sẵn help và auto-completion
- 3Khai báo [project.scripts]Thay entry mặc định bằng tên lệnh trỏ tới đối tượng app, ví dụ kiem_tra.main:app
- 4Build wheeluv build tạo wheel (kèm bản sdist) trong thư mục dist/, giao file wheel cho khách
- 5Khách cài cô lậppipx hoặc uv tool install để dùng lâu dài, uvx để chạy tạm
Thứ cần giao là một lệnh cài trong môi trường cô lập, không phải một file .py kèm lời dặn.
Đồ hoạ: FDE Times
Tuần thứ ba ở site khách, bạn có một script check_csv.py để kiểm tra file dữ liệu trước khi nạp vào pipeline.
Đội vận hành của khách nhắn hỏi cách chạy, và bạn trả lời bằng một đoạn hướng dẫn dài: cài Python bản nào, pip install những gì, đặt file ở đâu.
Hai ngày sau họ báo lỗi vì máy họ đang có một thư viện khác phiên bản.
Code không sai. Thứ hỏng là cách bàn giao. Thứ FDE nên để lại là công cụ khách tự chạy được sau khi mình đã rời site, và với việc chạy bằng dòng lệnh thì đó là một CLI được đóng gói đàng hoàng. Năm bước dưới đây đưa một script rời thành lệnh kiem-tra mà đội khách cài chỉ bằng một dòng.
Bạn sẽ dựng gì, và cần chuẩn bị gì?
Thành phẩm là một package Python tên kiem-tra. Package này có một lệnh nhận đường dẫn file CSV và tham số --max-rows, in kết quả ra màn hình và trả về mã lỗi khi file không đạt. Trên máy bạn cần có Python và uv. Phần logic kiểm tra trong ví dụ đã được lược bỏ, bạn thay bằng logic thật của mình.
Công cụ chính là Typer, thư viện tự giới thiệu là giúp xây CLI tốt, dễ viết và dựa trên type hint của Python.
Typer dựa trên type hint chuẩn của Python, nên bạn khai báo tham số giống như khi viết một hàm bình thường. Help và auto-completion cho Bash, Zsh, Fish, PowerShell thì có sẵn.
Đội khách được hưởng phần đó mà bạn không phải viết thêm dòng nào.
Bước 1–2: tạo package và viết lệnh
Hướng dẫn đóng gói của Typer dùng uv để tạo khung package:
uv init --package kiem-tra
cd kiem-tra
Lệnh này tạo sẵn pyproject.toml, file src/kiem_tra/__init__.py có một hàm main() in lời chào, và một entry mặc định kiem-tra = "kiem_tra:main" trong [project.scripts]. Entry này sẽ được thay ở bước 3.
Kiểm tra: mở pyproject.toml và tìm bảng [build-system]. Python Packaging User Guide yêu cầu bảng này lúc nào cũng phải có, dù bạn dùng build backend nào.
Tiếp theo, chạy uv add typer để thêm Typer vào dependencies, rồi tạo file src/kiem_tra/main.py. Đoạn dưới là bản tối giản: khung lệnh đầy đủ, chỉ phần logic kiểm tra được để trống cho bạn điền vào.
from pathlib import Path
import typer
app = typer.Typer()
@app.command()
def check(path: Path, max_rows: int = 1000):
"""Kiểm tra file CSV trước khi nạp vào pipeline."""
ok = True # thay bằng logic kiểm tra thật
if not ok:
raise typer.Exit(code=1)
typer.echo(f"{path}: đạt")
Type hint Path và int cho Typer biết kiểu dữ liệu của từng tham số. Còn việc tham số nào bắt buộc thì theo đúng quy tắc của hàm Python: path không có giá trị mặc định nên phải truyền vào, còn max_rows có = 1000 nên có thể bỏ qua. Hãy chạy --help để xem Typer hiển thị hai tham số này ra sao.
Bước 3: khai báo lệnh, chỗ hay sai nhất
Theo Python Packaging User Guide, muốn package cài kèm một lệnh thì phải khai báo lệnh đó trong bảng [project.scripts]. Ví dụ của Typer có dạng rick-portal-gun = "rick_portal_gun.main:app". Với package của bạn, sửa đúng dòng mặc định mà uv init đã tạo, đừng thêm dòng thứ hai trùng tên lệnh:
[project.scripts]
kiem-tra = "kiem_tra.main:app"
Vế trái là tên lệnh người dùng sẽ gõ, viết bằng gạch ngang. Vế phải là đường dẫn import, viết bằng gạch dưới, và phải trỏ tới đối tượng app chứ không trỏ tới hàm check.
Lý do nằm ở cách entry point hoạt động. Theo Packaging Guide, chạy lệnh này tương đương với việc import đối tượng được trỏ tới, gọi nó, rồi đưa kết quả trả về vào sys.exit.
Lời gọi đó không truyền tham số nào. Vì vậy, nếu bạn trỏ thẳng vào check, nhiều khả năng lệnh sẽ lỗi ngay khi chạy vì thiếu tham số, thay vì đi qua Typer để đọc dòng lệnh.
Cơ chế này còn cho thấy một điều: mã thoát của CLI là hợp đồng giữa bạn và hệ thống của khách. Cron job, CI hay script điều phối của họ sẽ đọc con số đó để quyết định có chạy tiếp hay không.
Kiểm tra: tạm đổi ok = True thành ok = False để giả lập file hỏng, rồi chạy qua uv run (lệnh này tự cài package vào môi trường của dự án trước khi chạy):
uv run kiem-tra --help
uv run kiem-tra du_lieu_hong.csv
echo $? # mong đợi 1, không phải 0
Bước 4–5: build wheel và giao theo cách khách không phải lo
uv build
Lệnh này tạo hai file trong thư mục dist/: một bản sdist .tar.gz và một file wheel .whl. File wheel là thứ bạn gửi cho khách hoặc đẩy lên kho package nội bộ của họ. Đến đây câu hỏi không còn là chuyện kỹ thuật nữa: máy của đội khách đang được thiết lập ra sao?
Câu hỏi đó quan trọng, vì lỗi ở đầu bài sinh ra từ việc cài chung vào Python của hệ thống.
Tài liệu uv giải thích rằng uv tool install cài mỗi công cụ vào một môi trường riêng, để phụ thuộc của tool, script và dự án không xung đột với nhau. pipx làm việc tương tự với các ứng dụng Python dành cho người dùng cuối: mỗi ứng dụng có virtualenv riêng và lệnh được đưa lên PATH.
Còn uvx chạy tool trong một môi trường tạm, cô lập, nên người dùng không phải cài trước. Với file wheel vừa build, hai cách gọi tương ứng là:
uv tool install ./dist/kiem_tra-0.1.0-py3-none-any.whl
uvx --from ./dist/kiem_tra-0.1.0-py3-none-any.whl kiem-tra du_lieu.csv
Cài lâu dài (pipx, uv tool install)
- Đội vận hành dùng lệnh hằng ngày
- Lệnh nằm sẵn trên PATH, gọi được từ cron hoặc script
- Cần có quy trình nâng cấp khi bạn ra bản mới
Chạy tạm (uvx)
- Analyst chỉ thỉnh thoảng mới chạy
- Chạy xong không để lại gì trên máy
- Hợp để demo hoặc cho khách dùng thử
Ở phía khách, nên chốt một quy tắc: ai dùng lệnh mỗi ngày thì cài lâu dài, ai chỉ chạy thử thì dùng uvx. Ghi rõ cả hai cách trong README, mỗi cách một dòng lệnh. Đừng bắt họ đọc hướng dẫn cài Python.
Những lỗi khiến công cụ chết sau khi bạn rời site
Lỗi đầu tiên là đặt tên lệnh quá chung chung, kiểu check hay run, rồi trùng với một lệnh có sẵn trên máy khách. Nên đặt tên gắn với nghiệp vụ, như kiem-tra-don-hang.
Lỗi tiếp theo là code lúc nào cũng thoát với mã 0, kể cả khi đã phát hiện dữ liệu hỏng. Người ngồi trước màn hình thì thấy dòng cảnh báo, nhưng pipeline của khách vẫn cứ thế nạp file hỏng.
Lỗi thứ ba là để khách cài wheel thẳng vào Python của hệ thống, đúng như tình huống đầu bài. Công cụ của bạn khi đó dùng chung thư viện với mọi thứ khác trên máy, và chỉ cần một lần nâng cấp là vỡ. README chỉ nên ghi lệnh pipx, uv tool install hoặc uvx, không ghi pip install trần.
Lỗi cuối cùng là chỉ test trên máy mình, nơi mọi phụ thuộc đã có sẵn. Trước khi gửi, hãy cài wheel vào một máy hoặc container sạch bằng đúng lệnh bạn ghi trong README.
Kỹ năng này hiện ra thế nào trong công việc FDE?
Ở site khách, khoảng cách giữa “demo chạy được” và “khách tự vận hành được” thường nằm ở những việc nhỏ như thế này. Một CLI có --help rõ ràng, có auto-completion và trả mã thoát đúng sẽ giảm hẳn số tin nhắn hỏi “chạy thế nào” sau khi bạn rời đi. Khách cũng đưa được nó vào cron hay CI mà không cần gọi bạn.
Khi đọc mô tả công việc FDE, hãy để ý những yêu cầu về công cụ nội bộ hay bàn giao cho đội khách tự vận hành: đó là chỗ kỹ năng này được dùng.
Trong CV, đừng viết “biết Python packaging”. Hãy viết rằng bạn đã đóng gói một công cụ kiểm tra dữ liệu thành CLI để đội vận hành của khách tự chạy hằng ngày, và nói rõ ai đã dùng nó.
Script nên đóng gói đầu tiên là script mà tuần này bạn phải giải thích cách chạy nhiều lần nhất.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!