🎯本篇成果:一个命令行待办清单。
添加、查看、完成、删除,数据存进文件,关掉终端重开还在。含注释120行左右,五脏俱全,没有一行多余。
📌 太长不看版(给想直接看代码的你)
| 项目信息 | 一句话说明 |
|---|---|
| 项目名称 | 命令行待办清单(Todo CLI) |
| 代码行数 | ~120行(含注释) |
| 依赖 | 零依赖,全部Python自带模块 |
| 核心功能 | 增删改查 + JSON持久化 |
| 跑起来的命令 | python main.py add "写一篇笔记" |
| 技术点 | argparse、json、pathlib、strftime |
| 做完你能得到 | 第一个“关掉终端再开数据还在”的程序 |
一、成品预览:先看我们要做什么
$ python main.py add 给专栏写一篇笔记 已添加:给专栏写一篇笔记 $ python main.py add 复习第3篇的venv命令 已添加:复习第3篇的venv命令 $ python main.py list 1. ⬜ 给专栏写一篇笔记(2026-09-01 20:00) 2. ⬜ 复习第3篇的venv命令(2026-09-01 20:01) —— 已完成 0/2 —— $ python main.py done 1 完成:给专栏写一篇笔记 $ python main.py list 1. ✅ 给专栏写一篇笔记(2026-09-01 20:00) 2. ⬜ 复习第3篇的venv命令(2026-09-01 20:01) —— 已完成 1/2 —— $ python main.py del 2 已删除:复习第3篇的venv命令 $ python main.py list 1. ✅ 给专栏写一篇笔记(2026-09-01 20:00) —— 已完成 1/1 ——🚀关键卖点只有一个:关掉终端,明天再开,
list一敲,数据还在。这叫持久化——之前你写的一切脚本都是“运行即失忆”,从今天起你的程序有了记忆。
🤔 为什么选待办清单当第一个项目?
| 理由 | 说明 |
|---|---|
| 功能小 | 一晚上跟得完,不劝退 |
| 覆盖全 | 增删改查 + 文件读写 + 命令行参数 = 企业级应用的骨架 |
| 零依赖 | 全部用Python自带模块,不装任何第三方包 |
| 能复用 | 学完这个,后面FastAPI版的待办清单(第23篇)你会看到同一个需求怎么“长大” |
二、前置知识:两分钟补两块砖
🔑 JSON:人能读懂的数据格式
就是长这样的文本文件,和Python的列表/字典几乎一一对应:
[{"text":"给专栏写一篇笔记","done":true,"created":"2026-09-01 20:00"},{"text":"复习第3篇的venv命令","done":false,"created":"2026-09-01 20:01"}]| 函数 | 作用 |
|---|---|
json.loads(文本) | JSON文本 → Python对象 |
json.dumps(对象) | Python对象 → JSON文本 |
💡 两个注意:JSON的列表对应
[]、字典对应{};ensure_ascii=False参数能让中文原样保存,而不是变成\u4e2d\u6587。
📁 pathlib:现代的文件操作
第5篇见过的Path,本篇用到三个技能:
| 用法 | 作用 |
|---|---|
Path("todos.json") | 定位文件(在当前目录) |
.exists() | 判断文件是否存在 |
.read_text()/.write_text() | 读写文本内容 |
💡 有了这两块砖,开工!
三、第0步:3分钟项目setup(前5篇的复习)
按第5篇模板建项目,一气呵成:
1. 新建文件夹 todo_project,VSCode打开(File → Open Folder) 2. python -m venv venv # 零依赖也建venv,习惯要从第一个项目养成 3. 激活:windows: venv\Scripts\activate(cmd) | .\venv\Scripts\Activate.ps1(PowerShell) mac: source venv/bin/activate 4. git init,写好.gitignore(venv/ 和 __pycache__/) 5. 新建空文件:main.py🤔 有同学问:为什么不分
utils/?故意的。第一版先把它写“歪”,后面复盘时我们亲手把它拆开重构——到时候你会真正明白“为什么要拆”,而不是背口诀。
四、第1步:设计数据结构(写代码前先想清楚)
一条待办长什么样?三个字段:内容、状态、创建时间。整个清单就是这样的列表:
# 概念示意,不用敲[{"text":"给专栏写一篇笔记","done":false,"created":"2026-09-01 20:00"},{"text":"复习第3篇的venv命令","done":false,"created":"2026-09-01 20:01"}]🎯先定结构再动手,是和"想到哪写到哪"的第一个分水岭。之后所有函数都围绕这个结构工作——结构即契约,第9节报错⑤会让你见识破坏契约的后果。
五、第2步:存与读——程序的记忆
"""todo —— 命令行待办清单 v1"""importjsonimportargparsefrompathlibimportPathfromtimeimportstrftime DATA_FILE=Path("todos.json")# 数据文件就放项目根目录defload_todos():"""读数据;第一次使用(文件不存在)返回空列表"""ifDATA_FILE.exists():returnjson.loads(DATA_FILE.read_text(encoding="utf-8"))return[]defsave_todos(todos):"""把列表写回文件;ensure_ascii=False让中文原样保存"""text=json.dumps(todos,ensure_ascii=False,indent=2)DATA_FILE.write_text(text,encoding="utf-8")📌 两个细节
| 细节 | 为什么重要 |
|---|---|
encoding="utf-8" | Windows默认编码是GBK,不加=中文乱码(第2篇的老规矩) |
indent=2 | 把文件排版成带缩进的人类可读格式——你可以用VSCode直接打开看 |
六、第3步:四个功能,每个10行以内
添加
defadd_todo(text):todos=load_todos()todos.append({"text":text,"done":False,"created":strftime("%Y-%m-%d %H:%M")})save_todos(todos)print(f"已添加:{text}")查看列表
deflist_todos():todos=load_todos()ifnottodos:print("清单是空的,来一条:python main.py add 学Python")returnfori,tinenumerate(todos,start=1):flag="✅"ift["done"]else"⬜"print(f"{i}.{flag}{t['text']}({t['created']})")done_count=sum(1fortintodosift["done"])print(f"—— 已完成{done_count}/{len(todos)}——")标记完成
defcomplete_todo(index):todos=load_todos()try:t=todos[index-1]# 用户编号从1开始,列表下标从0开始exceptIndexError:print(f"没有第{index}条,先用list看看")returnt["done"]=Truesave_todos(todos)print(f"完成:{t['text']}")删除
defdelete_todo(index):todos=load_todos()try:t=todos.pop(index-1)exceptIndexError:print(f"没有第{index}条")returnsave_todos(todos)print(f"已删除:{t['text']}")📌 注意每个函数都是同一个节奏
读 → 改 → 存🎯 这个“读改存”循环就是所有数据类应用的原子结构——从待办清单到银行系统,骨架不变,变的只是规模。
⚠️index - 1是新手的经典坑(编号从1开始给人看,下标从0开始给程序用),第9节③专门讲它。
七、第4步:argparse接上命令行
最后一块:让程序听懂add、list这些指令。
defmain():parser=argparse.ArgumentParser(description="命令行待办清单")sub=parser.add_subparsers(dest="command",required=True)p=sub.add_parser("add",help="添加一条待办")p.add_argument("text",help="待办内容(带空格请加引号)")sub.add_parser("list",help="查看全部待办")p=sub.add_parser("done",help="标记完成")p.add_argument("index",type=int,help="编号,见list输出")p=sub.add_parser("del",help="删除待办")p.add_argument("index",type=int,help="编号,见list输出")args=parser.parse_args()ifargs.command=="add":add_todo(args.text)elifargs.command=="list":list_todos()elifargs.command=="done":complete_todo(args.index)elifargs.command=="del":delete_todo(args.index)if__name__=="__main__":main()📌 两个关键点
| 知识点 | 说明 |
|---|---|
sub.add_parser | 给程序装“子命令”:add、list各管各的 |
type=int | argparse自动把用户输入转成整数,转换失败会替你报错 |
if __name__ == "__main__" | 第5篇说的“入口仪式”,直接运行才执行 |
八、第5步:验收清单
保存main.py,然后逐条跑:
python main.py add 给专栏写一篇笔记 python main.py add 复习第3篇的venv命令 python main.py list python main.py done 1 python main.py list python main.py del 2 python main.py list✅ 最终验收
关掉终端,重开,再list一次——数据还在,持久化验收通过!
用VSCode打开todos.json看一眼,这就是你程序的“记忆”长什么样。
📦 提交Git
验收通过,按第4篇流程存档:
gitadd.gitcommit-m"完成待办清单v1:增删改查 + JSON持久化"🎉 你的GitHub上从此多了一个完整项目。
九、常见报错:这6个,都是新手第一次写项目的真实坑(重点!)
① 打印✅时报UnicodeEncodeError: 'gbk' codec can't encode character
🔍 原因:老式Windows终端默认GBK编码,装不下emoji。
✅ 解法(二选一):
- 终端执行
chcp 65001切UTF-8(第6篇⑤的回旋镖); - 把代码里的✅⬜换成
[√][ ],一劳永逸。
②json.decoder.JSONDecodeError: Expecting value
🔍 原因:todos.json的内容不是合法JSON——多半是你手动编辑时删了个逗号/括号,或者文件被改成了空文件。
✅ 解法:打开文件找语法错(少逗号、多括号);救不回来就删掉文件重新来,你的程序会自动重建。
💡 顺便体会:数据文件是程序的命根,改它要像改代码一样谨慎。
③ done的编号总是差一位
🔍 原因:你在自己的代码里写了todos[index]而不是todos[index - 1]。界面上给用户看的编号从1开始,Python列表下标从0开始——这叫off-by-one错误,程序员的百年老坑。
✅ 解法:定位这一行的最好办法——用第6篇的断点,看看index和实际点到的元素差在哪。
④add 去超市存进去只剩“去”
🔍 原因:终端把空格当成参数分隔符,去和超市被切开了。
✅ 解法:带空格的内容加引号:
python main.pyadd"去超市买牛奶"# ✅ 正确python main.pyadd去超市买牛奶# ❌ 只存了"去"💡 Windows的cmd/PowerShell都认双引号。
⑤KeyError: 'done'
🔍 原因:你手动编辑todos.json时删了某个字段。程序依赖的结构被破坏了——还记得第4节说的“结构即契约”吗。
✅ 解法:把字段补回去;以后想改数据结构,改代码和改数据要同步。
⑥ 只敲了python main.py add,蹦出一堆红字
报错内容:
error: the following arguments are required: text🔍 原因:这不是崩溃,是argparse在拦你——你少传了参数,它把正确用法(usage)都打印出来了。
✅ 解法:照着usage提示补上参数。
💡学会读argparse的自动提示,它写的说明书比你想象的细。
十、课后练习(做了才算会)
| # | 练习 | 难度 | 提示 |
|---|---|---|---|
| 1 | 加一个clear命令,一键删除所有已完成项 | ⭐⭐ | 列表推导式[t for t in todos if not t["done"]]+sub.add_parser |
| 2 | 给“完成”加时间戳:done时记done_at,list里已完成项显示完成时间 | ⭐⭐⭐ | 修改complete_todo函数,加字段 |
| 3 | 按流程把本项目推上GitHub,按模板补README | ⭐⭐ | 你的第一个“完整项目”,简历上可以写的那种 |
| 4(选做) | 把统计行改成进度条:[####------] 已完成2/5 | ⭐⭐⭐ | "#" * n + "-" * (5-n) |
📦 配套代码
完整main.py、示例数据与README已上传GitHub(07-todo-cli/todo_project):【https://gitee.com/xqshadow/python-project-in-practice】