FreeCampus Python

Find the Right Tool Without Memorizing Everything

Discover, inspect, compare, and verify built-in, standard-library, installed, and project-owned Python tools without relying on memory or guesswork.
python-foundations modules-environments-projects standard-library documentation
Open in Colab
  • Level: Python Foundations
  • Estimated time: 3–4 hours
  • You will learn: Search from the smallest dependency outward, inspect callable contracts, distinguish import packages from distributions, diagnose name shadowing, and justify whether to reuse or write a tool.
  • Practice in: Google Colab, JupyterLab, or a local Python interpreter

A constellation report contains labels such as " Vega-North ". You need to remove surrounding spaces, normalize the case, count repeated labels, and find the most common one. You could write every operation yourself, but Python already supplies most of them. The professional skill is not memorizing a giant catalog. It is finding a plausible tool, reading its contract, and checking that the contract matches your data.

Answer these questions as you work:

1. Search from the nearest tool outward

Use a reuse ladder. Stop at the first level that solves the complete problem clearly enough:

  1. operations already available on the value, such as string methods;
  2. built-in functions such as len, sum, sorted, and enumerate;
  3. modules in Python’s standard library;
  4. an already-declared project dependency;
  5. a carefully evaluated new distribution; or
  6. a small function owned and tested by the project.

The order reduces unnecessary code and dependencies, but it is not a command to force an awkward built-in solution. Readability and the full contract still matter.

raw_labels = ["  Vega-North  ", "lyra-east", "VEGA-NORTH"]

normalized = [label.strip().casefold() for label in raw_labels]
print(normalized)

strip and casefold are methods already carried by each string. The list comprehension calls them in order for every label. strip() removes surrounding whitespace; casefold() performs an aggressive, Unicode-aware normalization for caseless matching. If the requirement were only display capitalization, casefold() would be the wrong contract even though the code runs.

Now count without writing a loop that manages a dictionary manually:

from collections import Counter

counts = Counter(normalized)
print(counts)
print(counts.most_common(1))

collections belongs to the standard library, and Counter is a dictionary subclass designed for counting hashable values. most_common(1) returns a list of up to one (value, count) pair—not the value by itself. That return shape is part of the contract.

Start close to the value and move outward only when the earlier level does not meet the requirement.

flowchart LR
  method["Value method"] --> builtin["Built-in function"]
  builtin --> stdlib["Standard library"]
  stdlib --> declared["Declared dependency"]
  declared --> evaluate["Evaluate new distribution"]
  evaluate --> project["Project-owned code"]

The final choice still needs a small verification example.

2. Inspect a callable before trusting its name

A useful name suggests intent, but it cannot state every accepted argument, return shape, or failure. Investigate in layers:

import inspect

print(callable(str.removeprefix))
print(inspect.signature(str.removeprefix))
print(str.removeprefix.__doc__)

callable(...) answers whether Python can call the object. The signature shows that a string instance and a prefix are involved. The docstring gives a compact behavior description. In a notebook, help(str.removeprefix) presents related information in a pager or output cell:

help(str.removeprefix)

Read parameter markers rather than skipping them. For example:

import inspect

print(inspect.signature(sorted))

The signature is sorted(iterable, /, *, key=None, reverse=False). The slash means arguments before it are positional-only. The star means key and reverse must be named. These calls therefore communicate different things:

stars = ["Sirius", "sun", "Betelgeuse"]

print(sorted(stars))
print(sorted(stars, key=str.casefold))
print(sorted(stars, key=len, reverse=True))

The first uses default text ordering. The second compares caseless keys without changing the original strings. The third orders by length from greatest to least. sorted returns a new list; it does not rearrange stars.

Ask the tool one narrow question

Documentation reading becomes reliable when paired with a miniature experiment:

samples = [
    ("north-star", "north-"),
    ("star-north", "north-"),
    ("north-north", "north-"),
]

for value, prefix in samples:
    print(value, "->", value.removeprefix(prefix))

This reveals three boundaries: only the beginning is considered, a missing prefix leaves the value unchanged, and exactly one matching prefix is removed. The experiment is not a substitute for documentation; it checks your reading against representative values.

Checkpoint: read a callable contract

3. Follow documentation from overview to proof

Good documentation has layers. A module index helps you discover a candidate; an API reference defines parameters and return values; a tutorial or example shows composition; and a small local check confirms the part your project depends on.

Suppose the constellation report needs an average observation count:

from statistics import mean

observations = [3, 4, 8, 5]
average = mean(observations)

print(average)
print(type(average).__name__)

Do not stop at “mean averages numbers.” Ask about empty data and accepted numeric types. A controlled failure makes one boundary visible:

from statistics import StatisticsError, mean

try:
    mean([])
except StatisticsError as error:
    print(type(error).__name__, error)

The caller must now decide whether an empty collection is invalid, means “no result,” or should be handled earlier. The standard library cannot choose the application rule.

TipRecord a dependency-sized note

Do not copy an entire reference page into project notes. Record the qualified name, the contract detail your code relies on, one representative example, one important boundary, and a link to the primary documentation.

Read exceptions as part of the API

Different parsing tools make different promises:

from pathlib import Path

candidate = Path("reports") / "night.txt"
print(candidate.suffix)
print(candidate.with_suffix(".json"))

Try an invalid suffix separately:

from pathlib import Path

try:
    Path("night.txt").with_suffix("json")
except ValueError as error:
    print(type(error).__name__, error)

Knowing that the suffix must begin with a dot helps you validate or construct the value at the right boundary. Exceptions are not footnotes; they describe which inputs are outside the callable’s promise.

Compare neighboring tools with one discriminating example

Search results often offer several plausible names. Do not choose the shortest name or the first result. Find an input on which the contracts differ. For caseless matching, lower() and casefold() often agree on ASCII text:

for value in ["VEGA", "Lyra"]:
    print(value.lower(), value.casefold())

That example cannot distinguish them. A better probe contains text for which Unicode caseless matching matters:

value = "STRAẞE"
print("lower:   ", value.lower())
print("casefold:", value.casefold())

The results are not interchangeable for every language. The documentation says casefold is intended to remove case distinctions and can be more aggressive than lower. That makes it a candidate for comparison keys, while lower may better preserve a display-oriented transformation. The project must still decide whether Unicode caseless matching is its real requirement.

Also compare mutation and return contracts, not only names:

labels = ["vega", "Orion", "lyra"]
new_order = sorted(labels, key=str.casefold)
returned = labels.sort(key=str.casefold)

print("new:", new_order)
print("original after sort:", labels)
print("sort returned:", returned)

sorted accepts any iterable and returns a new list. list.sort mutates one list and returns None. Both order these labels, but only one matches a contract that must preserve the caller’s original list. A discriminating example turns “these tools sound similar” into an observable design decision.

4. Separate import names from distribution names

Three categories are often confused:

  • A module is one importable unit, often one .py file.
  • An import package is an importable namespace that can contain modules and subpackages.
  • A distribution package is a versioned artifact installed by a package installer. It can provide one or several import packages.

The spelling need not match. The distribution PyYAML, for example, commonly provides the import package yaml. Installation commands name distributions; import statements name modules or import packages.

Use find_spec to ask whether the import system can locate a name without executing that module:

from importlib.util import find_spec

for import_name in ["json", "statistics", "not_a_real_star_module"]:
    specification = find_spec(import_name)
    print(import_name, "found" if specification else "missing")

A found specification says the import system found a candidate. It does not prove that the module is trustworthy, compatible with your use, or safe to import. To inspect installed distribution metadata, use importlib.metadata:

from importlib.metadata import PackageNotFoundError, version

for distribution_name in ["pip", "definitely-not-a-distribution"]:
    try:
        print(distribution_name, version(distribution_name))
    except PackageNotFoundError:
        print(distribution_name, "not installed")

version() expects a distribution name. It does not accept every import name. When tooling has recorded the mapping, packages_distributions() can help connect the two:

from importlib.metadata import packages_distributions

providers = packages_distributions()
print("pip is provided by:", providers.get("pip", []))

Some imports have no distribution mapping, especially project source that has not been installed. Treat an empty result as evidence to investigate, not as proof that the import is from the standard library.

Checkpoint: distinguish what Python imports from what pip installs

5. Prove which file an import selected

Imagine a project file named statistics.py beside your program. import statistics may now load the project file instead of the standard-library module. This is shadowing: an earlier matching name hides the intended one.

The safest first question is “which file did Python load?”

import statistics

print(statistics.__name__)
print(statistics.__file__)

Most source modules expose __file__. Some built-in or unusual module loaders do not, so diagnostic code can use getattr(statistics, "__file__", None).

Create a disposable reproduction rather than adding a conflicting file beside your notebook:

import subprocess
import sys
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as temporary_name:
    workspace = Path(temporary_name)
    (workspace / "statistics.py").write_text(
        "origin = 'local shadow'\n",
        encoding="utf-8",
    )
    result = subprocess.run(
        [
            sys.executable,
            "-c",
            "import statistics; print(statistics.__file__); "
            "print(hasattr(statistics, 'mean'))",
        ],
        cwd=workspace,
        text=True,
        capture_output=True,
        check=True,
    )
    print(result.stdout)

The child process starts in the temporary workspace. Its first search location finds statistics.py; that file has no mean, so hasattr prints False. Renaming the local file to something project-specific, deleting its generated __pycache__, and restarting the process removes the ambiguity. Do not repair ordinary project imports by inserting guessed paths into sys.path; correct the name or installation contract.

Other useful origin checks are:

from importlib.util import find_spec

specification = find_spec("statistics")
print(specification.origin if specification else "not found")

If an import behaves differently in two terminals, record the current working directory, sys.executable, sys.path, the module’s origin, and the exact command. “Python cannot find it” is a symptom; those values are evidence.

6. Evaluate a new dependency before adding it

A third-party distribution can save months of work, but every dependency adds an installation, compatibility, maintenance, and trust decision. Evaluate the specific project need:

Question Evidence to collect
Does it solve the complete requirement? API reference and a small representative experiment
Is its Python/platform support compatible? Project metadata and supported-version policy
Is it maintained for the intended use? Release history, issue activity, and maintainer guidance
Is the license acceptable for the project? Declared license information and organizational policy
Is the dependency proportionate? Compare its delivered capability with a clear project-owned function
Can the team replace or contain it? Place it behind a small project interface when appropriate

Popularity alone is not a contract. Neither is the first search result. Prefer the project’s primary documentation and package-index metadata, then verify the small slice of behavior your code needs.

For this unit’s label normalization, string methods and Counter are enough. Adding a new distribution would increase setup work without improving the contract. A specialized astronomy coordinate conversion, however, may justify a mature domain library rather than a handwritten scientific implementation.

7. Build a tool-discovery dossier

Create a short record for this requirement:

Given labels containing surrounding spaces and mixed case, return each normalized label and the two most frequent labels. Equal counts should retain first-seen order.

Use these required names:

from collections import Counter


def normalize_label(value):
    """Return a stripped, caseless label suitable for matching."""
    return value.strip().casefold()


def summarize_labels(values, limit=2):
    """Return normalized labels and their most common `(label, count)` pairs."""
    normalized = [normalize_label(value) for value in values]
    return normalized, Counter(normalized).most_common(limit)

Run ordinary and boundary checks:

labels, leaders = summarize_labels(
    [" Vega ", "LYRA", "vega", "Orion", "lyra", "vega"]
)

assert labels == ["vega", "lyra", "vega", "orion", "lyra", "vega"]
assert leaders == [("vega", 3), ("lyra", 2)]
assert summarize_labels([], limit=2) == ([], [])
assert summarize_labels([" A ", "b"], limit=0) == (["a", "b"], [])
print(leaders)

Your dossier should contain:

  1. the reuse-ladder level for strip, casefold, and Counter;
  2. the signature or documented contract detail each use depends on;
  3. one boundary learned from an experiment;
  4. why no new distribution is justified; and
  5. a primary-documentation link.
ImportantChanged requirement

Suppose display spelling must be preserved while matching remains caseless. Do not overwrite every label with its casefolded form. Change the representation so comparison keys and first-seen display labels remain available. Explain why the original return contract can no longer satisfy both needs.

Compare one possible display-preserving extension
from collections import Counter


def most_common_display_labels(values, limit=2):
    """Return first-seen display labels paired with caseless counts."""
    display_by_key = {}
    keys = []
    for value in values:
        display = value.strip()
        key = display.casefold()
        keys.append(key)
        display_by_key.setdefault(key, display)

    counts = Counter(keys)
    return [
        (display_by_key[key], count)
        for key, count in counts.most_common(limit)
    ]


assert most_common_display_labels([" Vega ", "VEGA", "Lyra"]) == [
    ("Vega", 2),
    ("Lyra", 1),
]

The normalized key supports matching; display_by_key retains the first trimmed spelling. One value no longer has to serve two incompatible purposes.

Checkpoint: choose and verify a tool

8. Key points for finding reusable tools

  • Search value methods, built-ins, and the standard library before adding a new dependency, while still choosing for the complete requirement.
  • Read signatures, parameter markers, docstrings, return shapes, and documented exceptions; verify the relevant behavior with a small example.
  • A module or import package is what Python imports. A distribution is what an installer installs and versions; their names need not match.
  • Use find_spec, importlib.metadata, sys.executable, and module origin as diagnostic evidence rather than guessing.
  • A local file can shadow the intended module. Identify its origin before changing paths or installing packages.
  • New dependencies deserve a proportional compatibility, maintenance, license, trust, and replacement decision.

References

Back to top