简介这是一份面向 Python 初学者的命令行待办事项练习资源帮助读者在本地环境中通过实现增删任务、保存与加载数据等核心功能掌握 CLI 程序的基本开发思路。资源围绕 add_one_task、print_list、delete_task、save_todos、load_todos 五个待实现函数展开适合刚接触 Python 文件读写与命令行交互的练习者。压缩包共 7 个文件约 295KB包含 2 个 py 脚本应用入口与测试用例、1 个 json 配置、1 个 csv 数据文件、1 个 md 说明文档、1 个 gitignore 及 1 个 gif 演示图结构精简便于快速上手。已有 355 人学习关注。通过该资源读者可获得一套可直接运行的本地待办清单骨架借助 test.py 验证实现是否正确并参考 README 与演示动图理解预期交互效果从而在动手补全函数的过程中熟悉命令行参数处理、任务持久化与基础测试流程。1. 命令行里的待办清单一个把 Python 基础语法串起来的本地小工具很多人学 Python 卡在“语法都会就是写不出东西”这一步。函数、列表、字典、文件读写、异常处理单独拎出来都认识凑一起就不知道从哪下手。这个 Exercise-todo-list-cli-python 就是冲着这个痛点来的它把待办事项的增删改查全部塞进一个命令行程序里数据只存在本地文件不联网、不依赖数据库跑起来就是一个能天天用的清单工具。适合刚学完基础语法想找个完整项目练手的人也适合想拿它当脚手架、改成自己日常工具的人。整个项目没有花哨的框架纯标准库读源码不会被第三方依赖绕晕改起来也放心。2. 项目定位与技术选型为什么用标准库而不是上框架2.1 它到底解决什么问题待办清单这个需求本身不复杂但要把“能长期用”这件事做扎实就得考虑几个现实问题数据存哪、程序关了再开还在不在、输入错了会不会直接崩、多条任务怎么区分。这个项目选择把数据落到本地文件用命令行交互等于把复杂度压到最低。你不需要装数据库、不需要配环境变量、不需要起服务python todo.py就能跑。对于练习项目来说这种“零外部依赖”的设计反而更考验基本功——文件读写、数据结构设计、输入校验一个都躲不掉。从功能边界看它覆盖的是单机单用户的场景添加任务、查看列表、标记完成、删除任务、退出保存。没有多用户、没有云同步、没有优先级排序这些扩展功能。这不是缺陷是刻意收窄范围让代码量控制在能一口气读完的程度。常见做法是先用一个列表存任务每个任务用字典表示字段包括任务内容和完成状态退出时序列化写文件启动时反序列化读回来。2.2 技术栈拆解标准库够用就别加戏项目正文没有给出具体依赖清单但从“仅限本地”这个定位推断它大概率只用了 Python 标准库。下面这张表是我按这类项目的常见实现整理的模块分工你可以对照自己手上的源码看是否吻合模块用途典型调用json任务数据的序列化与反序列化json.load()/json.dump()os判断数据文件是否存在、拼接路径os.path.exists()sys处理退出、读取命令行参数sys.exit()datetime给任务打时间戳可选datetime.now()为什么用 JSON 而不是纯文本或 CSV因为任务本身是结构化的有内容有状态JSON 能原样保留嵌套结构读回来直接就是 Python 的 list 和 dict不用自己写解析逻辑。CSV 处理多字段还行但遇到任务内容里带逗号就容易翻车。纯文本更不用说状态和内容混在一起解析全靠字符串切割维护成本高。提示如果你拿到的源码用的是pickle而不是json注意 pickle 反序列化不可信文件有安全风险练习项目自己用没问题但别拿去读别人给的文件。2.3 目录结构长什么样这类 CLI 小项目的目录通常很扁平不会搞多层包结构。我见过的典型布局是这样Exercise-todo-list-cli-python/ ├── todo.py # 主程序入口包含交互循环 ├── tasks.json # 运行时生成的数据文件首次运行后出现 └── README.md # 使用说明有些版本会把逻辑拆成storage.py负责读写文件和cli.py负责交互但多数练习项目为了直观会全部写在一个文件里。单文件的好处是读起来连贯从main()进去一路看到底不用在多个文件之间跳。缺点是超过三百行之后会开始乱这时候再拆也不迟。2.4 数据模型一个任务该有哪些字段任务用字典表示是最自然的选择。最小字段集是task内容和done是否完成。如果想让清单更有用可以加created_at记录创建时间方便按时间排序或筛选。下面是一个典型的任务对象{ task: 写完项目解析笔记, done: False, created_at: 2025-01-15 10:30:00 }整个清单就是这些字典组成的列表。存文件时json.dump(tasks, f, ensure_asciiFalse, indent2)ensure_asciiFalse保证中文正常显示而不是变成\uXXXXindent2让文件可读出问题时能直接打开看。读文件时用json.load(f)拿到列表直接赋值给变量。这里有个细节如果文件不存在或内容为空要能优雅地返回空列表而不是抛异常让程序起不来。3. 核心模块拆解从启动到退出的完整链路3.1 程序入口与主循环命令行工具的心脏是一个while True循环不断打印菜单、读输入、分发处理。下面这段是这类项目最常见的骨架import json import os DATA_FILE tasks.json def load_tasks(): 从本地文件读取任务列表文件不存在或损坏时返回空列表 if not os.path.exists(DATA_FILE): return [] try: with open(DATA_FILE, r, encodingutf-8) as f: return json.load(f) except (json.JSONDecodeError, IOError): # 文件损坏或读不了不让程序崩退回空列表 return [] def save_tasks(tasks): 把任务列表写回本地文件 with open(DATA_FILE, w, encodingutf-8) as f: json.dump(tasks, f, ensure_asciiFalse, indent2) def main(): tasks load_tasks() while True: print(\n1. 查看任务 2. 添加任务 3. 标记完成 4. 删除任务 5. 退出) choice input(请选择操作: ).strip() if choice 1: show_tasks(tasks) elif choice 2: add_task(tasks) elif choice 3: mark_done(tasks) elif choice 4: delete_task(tasks) elif choice 5: save_tasks(tasks) print(已保存再见。) break else: print(无效选项请重新输入。) if __name__ __main__: main()逻辑说明load_tasks在程序启动时调用一次把磁盘上的数据读进内存之后所有增删改都在内存里的tasks列表上操作只有退出时才调save_tasks写回磁盘。这种“内存操作 退出落盘”的模式简单直接缺点是程序被强制杀掉时未保存的改动会丢。如果想更稳可以在每次修改后立即保存代价是频繁 IO。参数方面DATA_FILE用相对路径意味着你在哪个目录下运行程序数据文件就生成在哪个目录换目录跑会读不到之前的任务。常见做法是改成基于脚本位置的绝对路径BASE_DIR os.path.dirname(os.path.abspath(__file__)) DATA_FILE os.path.join(BASE_DIR, tasks.json)这样不管从哪个目录调用数据文件始终跟着脚本走。3.2 增删改查四个操作的具体实现查看任务最简单遍历列表打印序号和状态即可。关键是序号从 1 开始方便用户输入def show_tasks(tasks): if not tasks: print(清单是空的先添加一条吧。) return for i, t in enumerate(tasks, start1): status ✓ if t[done] else print(f{i}. [{status}] {t[task]})添加任务需要处理空输入避免把空字符串存进去def add_task(tasks): content input(输入任务内容: ).strip() if not content: print(内容不能为空。) return tasks.append({task: content, done: False}) print(f已添加: {content})标记完成和删除都依赖用户输入的序号这里最容易出问题的是序号越界和输入非数字。用try/except包住int()转换再检查范围def mark_done(tasks): show_tasks(tasks) try: idx int(input(输入要标记完成的任务序号: )) - 1 if 0 idx len(tasks): tasks[idx][done] True print(已标记完成。) else: print(序号超出范围。) except ValueError: print(请输入数字。)删除操作同理但要注意删除后列表索引会变所以每次删除都重新展示一遍。这四个函数加起来不到六十行却把列表操作、字典访问、异常处理、字符串格式化全练到了。如果你能把它们默写出来Python 基础就算过关了。3.3 数据持久化的边界情况文件读写看起来简单实际藏着不少坑。第一首次运行时tasks.json不存在load_tasks必须能处理这种情况否则直接FileNotFoundError。第二文件存在但内容为空比如手动清空了json.load会抛JSONDecodeError也要捕获。第三写入时如果磁盘满了或没有权限open会抛IOError虽然练习项目不常遇到但加上捕获不亏。第四编码问题Windows 上默认编码可能是 GBK读写中文会乱码所以open时显式指定encodingutf-8是必须的。注意如果你在 Windows 上用记事本打开tasks.json看到乱码别急着改代码先确认文件本身是不是 UTF-8 编码。用 VS Code 或 Notepad 打开右下角能看到编码格式。还有一个容易被忽略的点json.dump默认会把非 ASCII 字符转义成\uXXXX中文任务名存进去会变成一串编码。加上ensure_asciiFalse才能原样保存。这个参数不写功能上没问题但文件可读性差很多调试时打开文件一脸懵。4. 跑起来与改造从复现到加功能4.1 环境准备与首次运行这个项目对 Python 版本要求不高3.6 以上都能跑因为用到的都是很基础的语法。不需要pip install任何东西标准库自带。步骤就三步# 1. 确认 Python 版本 python --version # 2. 进入项目目录 cd Exercise-todo-list-cli-python # 3. 运行主程序 python todo.py如果系统里python命令指向 Python 2换成python3 todo.py。运行后你会看到菜单按提示输入数字选择操作。第一次添加任务后当前目录下会生成tasks.json用文本编辑器打开能看到结构化的任务数据。下次再运行之前的任务会自动加载出来。4.2 常见问题排查现象一运行报ModuleNotFoundError: No module named json。原因这种情况极少见通常是 Python 安装不完整或环境变量指向了错误的解释器。解决用which pythonLinux/macOS或where pythonWindows确认解释器路径换一个完整的 Python 安装。现象二添加中文任务后重新打开程序显示乱码。原因open时没指定encodingutf-8系统用了默认编码。解决检查load_tasks和save_tasks里的open调用确保都带了encodingutf-8。现象三输入序号后程序没反应或直接退出。原因int()转换失败抛了异常但没捕获或者序号越界导致索引错误。解决确认mark_done和delete_task里有try/except ValueError并且做了范围检查。现象四任务添加后退出程序再打开发现没了。原因退出时没有调用save_tasks或者save_tasks写到了别的目录。解决检查main循环里选择退出时是否调用了保存函数检查DATA_FILE路径是否基于脚本位置。现象五tasks.json内容变成一行很难读。原因json.dump没加indent参数。解决加上indent2文件会格式化输出方便手动查看和调试。4.3 三个低成本的改造方向第一个改造是加优先级。在任务字典里增加priority字段取值high/normal/low查看时按优先级排序。改动点集中在add_task多问一句优先级和show_tasks排序后展示。第二个改造是加截止日期用datetime模块解析用户输入的日期字符串存成 ISO 格式查看时高亮已过期的任务。第三个改造是把数据文件换成 SQLite用sqlite3标准库好处是支持并发和复杂查询坏处是代码量上去了对练习项目来说可能过度设计。我的建议是先做第一个因为改动最小、见效最快而且能让你体会到“数据结构一变周边函数都要跟着调”的连锁反应。这正是小项目练手价值最高的地方。5. 二次开发技巧把练习项目变成自己的工具如果你打算在这个项目基础上继续改有几个习惯能帮你少走弯路。第一改任何功能之前先跑一遍现有流程确认基线是通的。我见过太多人上来就加功能结果出了问题分不清是新代码的锅还是原来就有 bug。第二每次只改一个点改完立刻运行验证。命令行程序的好处是反馈快敲几个数字就能看到结果别攒一堆改动再一起测。第三数据文件格式一旦确定就别轻易改字段名。比如你把done改成completed旧数据文件读进来就会KeyError。如果非要改写个迁移函数读旧格式转成新格式再存回去。这个思路在真实项目里叫 schema migration练习项目里提前体会一下没坏处。第四给关键函数加类型注解。Python 是动态类型但加上注解之后IDE 能帮你检查出不少低级错误。比如def add_task(tasks: list, content: str) - None:写清楚参数和返回值过两周回来看代码不用重新推理。第五把交互逻辑和业务逻辑分开。现在add_task里既有input()又有列表操作想写单元测试就得模拟输入。更好的做法是拆成create_task(content)返回任务字典和add_task(tasks)负责交互这样测试直接调create_task就行不用碰输入输出。def create_task(content: str) - dict: 纯函数根据内容创建任务对象方便测试 return {task: content, done: False} def add_task(tasks: list) - None: 交互层负责读输入和调用 create_task content input(输入任务内容: ).strip() if not content: print(内容不能为空。) return tasks.append(create_task(content)) print(f已添加: {content})这个拆分看起来多此一举但当你任务多到需要批量导入、或者想从别的脚本调用这个逻辑时纯函数的价值就出来了。最后说个血泪经验别在while True循环里直接break退出而不保存。我早期写这类工具时图省事结果有次敲了几十条任务手滑按了 CtrlC全没了。从那以后我每次写命令行工具都在try/finally里强制走一遍保存逻辑或者干脆每次修改后立即落盘。数据无价多一次 IO 换一份安心这笔账怎么算都划算。希望这个项目能帮你把 Python 基础真正串起来也希望你改完之后它真能变成你每天会打开的工具。本文还有配套的精品资源点击获取