Embercode
✕
Learning with AILesson 12 of 16

45 min read

Keeping a project portfolio

Why this matters

A portfolio is a set of projects that another person can open and check. A hiring manager, a teammate or a client usually gives each project a few minutes. In that time they want to know three things. What does it do? Does it run? Did the person keep working on it? The README answers the first two. The changelog answers the third.

Many beginners leave these files out, or write one line that says "my calculator app". A reader who sees that line cannot tell whether the project works, how to start it, or what the author learned. A README with a real example and a changelog with one line per change make the work easy to judge.

This lesson covers those two files and how to check them. It does not cover how to publish a project online or how version control works. The Git lessons in this course cover those. The sample project is a word counter, a small command-line tool. A command-line tool runs when you type a command in a terminal, not when you click a window.

Words used in this lesson

  • Project folder: the folder that holds one project's files.
  • README: the file a reader opens first. It is usually named README.md. The .md ending means Markdown, a plain text format that uses simple marks such as # for a heading and - for a list item.
  • Changelog: a file that lists what changed in each version of a project. Its usual name is CHANGELOG.md.
  • Version: a number that names one release of a project, such as 0.2.0.
  • Release: a version that you hand to other people to use.
  • Version control: a system that records every change to a folder's files. Git is the common one.
  • Exit code: a number a program returns when it finishes. 0 means it finished without a problem. Any other number means it stopped with a problem.
  • Unit test: a small automatic check of one function. A test passes when the function gives the answer the test expects.

The sample project

The sample is a folder called wordcount. It has four files that a reader would see.

  • wordcount.py is the program.
  • test_wordcount.py holds the unit tests.
  • README.md explains the project.
  • CHANGELOG.md lists the changes.

The program counts the words in a text file. The core of it is one function:

def count_words(text):
    """Return the number of words in text. A word is a run of non-space characters."""
    return len(text.split())

split() breaks the text wherever there is a space, a tab or a line break. len counts the pieces. The rest of the file opens the file, calls this function, prints the result, and returns an exit code. A reader who wants to know what the program does should not have to read the code to find out. The README says it for them.

The README: what a reader needs in order

A README works like a shop window. The reader sees the first screen and decides whether to look inside. Put the most useful part first, and keep each section short.

Here is the top of the sample README.

# wordcount
 
A command-line tool that prints how many words are in a text file.
 
## What it does
 
Give it the path to a text file. It prints one number: the count of words in the file.
 
    python3 wordcount.py notes.txt
 
With the sample file `notes.txt`, which contains the two lines `Read the notes` and `and count them.`, the output is:
 
    6

The rest of the file has these sections, in this order.

  1. Title and one sentence. The title is the project name. The sentence says what the tool does in plain words, with no hype.
  2. What it does, with a real example. The command and its output. This is the most useful section, because the reader can see the result without installing anything.
  3. Requirements. What must be installed before the tool runs. Here, Python 3 and nothing else, because the program uses only the standard library, the parts of Python that come with it.
  4. Run the tests. The exact command to run the unit tests.
  5. Errors. What the reader will see when something goes wrong, and what it means.
  6. How it works. A short list of the functions and what each one does.
  7. Limits. What the tool does not handle.
  8. Changes. A link to the changelog.

Each section answers one question. A reader who only wants to run the tool stops after section 2. A reader who wants to change the code goes on to section 6.

Example 1: a command the reader can copy

The most common README mistake is a command that the reader cannot copy and run. The sample README gives the command and the output, so the reader can check their own result against it.

Here is what happens when you run it on the sample file.

python3 wordcount.py notes.txt
6

The number is correct for the file. The file has Read, the, notes, and, count and them, which is six words. Check the example in this way before you publish. Copy each command from the README, paste it into a terminal in a clean folder, and compare the output with what the README says. If they differ, fix the README or the program, whichever is wrong.

The tests work the same way. The README tells the reader to run this command.

python3 -m unittest -v
test_counts_words_separated_by_spaces (test_wordcount.CountWordsTests.test_counts_words_separated_by_spaces) ... ok
test_empty_text_has_no_words (test_wordcount.CountWordsTests.test_empty_text_has_no_words) ... ok
test_newlines_and_repeated_spaces_do_not_add_words (test_wordcount.CountWordsTests.test_newlines_and_repeated_spaces_do_not_add_words) ... ok
 
----------------------------------------------------------------------
Ran 3 tests in 0.000s
 
OK

The time after Ran will differ on your machine. The last line, OK, is the part the reader looks for. If a test fails, the last line says FAILED and the output shows which test failed and why. A README that says "run the tests" without the command makes the reader guess. Put the command in a code block, as above.

Example 2: an errors section that prevents false alarms

Readers often think a program is broken when it is only reporting a bad input. An errors section tells them in advance what the messages mean. The sample README has one, and its entries match the real output of the program.

A missing file gives a message and exit code 1.

python3 wordcount.py missing.txt; echo "exit code: $?"
wordcount: cannot read missing.txt: No such file or directory
exit code: 1

Running the program with no file name gives a usage line and exit code 2.

python3 wordcount.py; echo "exit code: $?"
usage: python3 wordcount.py FILE
exit code: 2

The error messages go to the error output, a separate stream from normal output. A reader who redirects normal output to a file still sees the error on the screen. The README does not need to explain streams. It needs to show what the reader will see and what to do about it. For the missing file, the answer is to check the file name. For the usage line, the answer is to add a file name after the command.

The echo "exit code: $?" part is only there to show the number. $? is a shell variable that holds the exit code of the last command. A README does not need it. It is used here to make the example visible.

Example 3: a changelog that tells the story

The changelog is a list of changes, newest first. A reader can see how the project grew, and a future version of you can see why a change was made. Here is the sample changelog. The history in it was made up to show the format.

# Changelog
 
All notable changes to this project are listed here. The newest version is at the top.
 
## [0.2.0]
 
### Added
 
- A clear error message when the file cannot be read, with exit code 1.
- A usage message and exit code 2 when no file name is given.
 
## [0.1.0]
 
### Added
 
- Count the words in a UTF-8 text file and print the number.
- Unit tests for the counting rule.

Each version has a heading with its number. Under it, changes are grouped into a few standard headings: Added for new features, Changed for changes to how something works, Fixed for bugs that are gone, and Removed for features that are gone. You use the ones that apply.

Version numbers follow a common pattern with three parts, such as 0.2.0. The first part changes when a release breaks something that worked before. The second part changes when a release adds a feature. The third part changes for a bug fix. Projects that are still in early development often keep the first number at 0 until they are stable.

Keep an Unreleased heading at the top for changes that are not in a release yet. When you publish a release, rename that heading to the version number and add a new empty Unreleased heading above it.

Each entry should say what a user would notice. "Refactored the tax code" tells a reader nothing they can use. "A clear error message when the file cannot be read" tells them what to expect. Write one line per change. Write the line in the same step as the change, not a month later, when you no longer remember what you did.

Writing the README from real runs

Write the README after the program works, and write it from what you actually ran, not from memory. Memory tends to add features that were planned and never built, and to leave out the error you hit on the way.

  1. Run the program on the example input and copy the output exactly. For notes.txt the output is 6, so that is what the README shows.
  2. Check each requirement with a command. The sample needs Python 3. On the machine used for this lesson, the check is:
python3 --version
Python 3.14.4

The README then says "Python 3, tested with Python 3.14.4". Writing the version you tested tells the reader what is known to work, without claiming more.

  1. Write the errors section by causing each error on purpose. The two errors in Example 2 came from a missing file name and a file that does not exist. Copy the messages into the README exactly, because a reader who gets stuck searches for the words they see on screen.
  2. Run every command in the README once more from a clean folder. A clean folder holds only the project files and no output from earlier runs. The __pycache__ folder from the mistakes section would not be there, so a command that only works because of leftover files would fail here and show you the problem.

Example 4: a release

A changelog keeps the Unreleased heading at the top while work is being done. Suppose the usage message from version 0.2.0 is changed to go to the error output, and the change is not yet released. The entry sits under Unreleased:

## [Unreleased]
 
### Changed
 
- The usage message is printed to the error output, not the normal output.
 
## [0.2.0]

Now suppose you release it. The change is small and does not change how the tool is used, so only the third number of the version moves. The heading takes the new number, and a fresh Unreleased heading goes above it:

## [Unreleased]
 
## [0.2.1]
 
### Changed
 
- The usage message is printed to the error output, not the normal output.
 
## [0.2.0]

The Unreleased heading is never removed. It is the place for the next change, so a reader can see what is coming. If you tag or publish the release in version control, use the same number in the tag, so that the changelog and the release point to the same code.

Common mistakes

A README with no output to compare. A README that says "run it to count the words" leaves the reader with nothing to check. Show the exact command and the exact output, as in Example 1.

A README command that does not work on the reader's computer. The README says python wordcount.py notes.txt. On the machine used for this lesson, that command does not exist:

python wordcount.py notes.txt; echo "exit code: $?"
Command 'python' not found, did you mean:
  command 'python3' from deb python3
  command 'python' from deb python-is-python3
exit code: 127

Exit code 127 means the shell could not find the command. Some systems use python and others use python3. Write the command that works on the machine you tested, and say in Requirements which Python version is needed. A reader who has the other name can then adapt the command.

A wrong file name in the README example. If the README's example file is not in the folder, the reader gets an error with the name they typed:

python3 wordcount.py missing.txt; echo "exit code: $?"
wordcount: cannot read missing.txt: No such file or directory
exit code: 1

Ship the example file with the project, as the sample does with notes.txt, so the README example runs as written.

Generated files left in the project. After running the tests, the folder contains a folder that Python created on its own:

find . -type f | sort
./CHANGELOG.md
./README.md
./__pycache__/test_wordcount.cpython-314.pyc
./__pycache__/wordcount.cpython-314.pyc
./notes.txt
./test_wordcount.py
./wordcount.py

The __pycache__ folder holds compiled copies of the Python files. Python rebuilds it when needed, so it does not belong in the project's history. Add a file called .gitignore to the folder, with one line that names the folder, so that version control skips it:

__pycache__/

A changelog that says nothing. An entry such as "Various fixes" does not tell a reader which bugs were fixed or whether they affect them. Name the change in plain words, as in Example 3.

Hype instead of facts. A line such as "a fast and powerful word counter" makes a claim with no way to check it. A line that says "counts the words in a 2-line file in under a second" can be tested. If you have not measured speed, leave speed out.

Practice

Exercise 1. The README says "Run python3 wordcount.py to start the program." Rewrite the line so that a reader can copy the command and see what happens.

Show the answer

Write the command in a code block, with a file name, and show the output: "Run the command below on any text file. It prints the number of words in the file." Then the block holds python3 wordcount.py notes.txt and the output 6. A reader can now copy the command, run it on the sample file, and compare the result.

Exercise 2. The changelog has the entry "Updated code." Rewrite it so that a reader can see what changed.

Show the answer

Use the heading for the kind of change and say what the user sees. For example: under Added, "A clear error message when the file cannot be read, with exit code 1." The entry names the new behaviour and the exit code, so a reader can check it with the commands from Example 2.

Exercise 3. After you run the tests, which folder should go in .gitignore, and why?

Show the answer

The __pycache__ folder. Python creates it to store compiled copies of the files, and it can be rebuilt at any time. It is not part of the project, so the .gitignore file lists __pycache__/ to keep it out of version control.

Exercise 4. You fix a bug in the program after version 0.2.0 was released. Which heading does the entry go under, and what should the version be?

Show the answer

The entry goes under Fixed. While the fix is not yet released, it goes under Unreleased at the top. When you release it, the heading is renamed to the next version. A fix alone uses the third number, so the release is 0.2.1.

Exercise 5. Look at the README's "What it does" section. Name the three things a reader must find on the first screen, and say why each one matters.

Show the answer

The first is what the project does, in one sentence, so the reader knows whether it fits their need. The second is the command to run, so they can try it. The third is the output that command gives, so they can check that their own result matches. Without the output, a reader who runs the command cannot tell whether it worked.

Summary

  • A README answers three questions: what the project does, whether it runs, and how it works. Put the example and its output near the top.
  • Copy every command from the README into a clean folder and compare the output with what the README shows before you publish.
  • An errors section tells the reader what the messages mean, so they do not think a bug is a crash.
  • A changelog lists changes newest first, grouped under Added, Changed, Fixed and Removed, with one plain line per change.
  • Write each changelog line in the same step as the change, and keep an Unreleased heading for work that is not released yet.
  • Keep generated folders such as __pycache__ out of the project with a .gitignore file.
Check yourself: Portfolio (8 questions) →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Reading other people's code