AI Coding Assistants: How to Use Them Effectively

AI coding assistants write plausible code quickly, and plausible is not the same as correct. This guide summarises what research says about their speed and security, then has a small local model write six functions, scores them with hidden tests, and shows what examples and test feedback fix.

GloryTecks Team

Training & Placement Faculty

6 May 2026 9 min read

Last updated: 3 October 2026

Share:

AI coding assistants are now part of most developers' day. In the 2025 Stack Overflow Developer Survey, 84% of respondents used or planned to use AI tools in their work, and 51% of professional developers used them daily. Yet more of them distrusted the accuracy of those tools, 46%, than trusted it, 33% (Stack Overflow: 2025 survey).

Both numbers are right. Assistants are useful and often wrong. Using them effectively means building your workflow around catching the wrong parts.

What the evidence says

Speed is not guaranteed. In a randomised controlled trial, METR asked 16 experienced open-source developers to complete 246 real tasks in projects they knew well, with and without AI tools. Allowing AI made them 19% slower. The developers had expected a 24% speed-up and, afterwards, believed they had been 20% faster (Becker et al.: METR study). That was one setting, experts in familiar code, but it shows how unreliable our impression of our own productivity can be.

Security needs checking. An early study of GitHub Copilot generated 1,689 programs across 89 scenarios built around common weaknesses, and found about 40% vulnerable (Pearce et al.: Asleep at the Keyboard?). Models have improved since, but generated code still inherits patterns from the code it learned from.

A small experiment

The script asks a small local model to write six functions, each in three ways: from a description only, from a description plus example tests, and after one round of feedback listing the tests that failed. Every version runs in a separate process against hidden tests that include edge cases the model never saw.

python
import json
import re
import subprocess
import sys
import urllib.request

# Six small functions, each with examples a developer might show and hidden tests that include edge cases
TASKS = {
    "slugify": ("slugify(title: str) -> str: lower-case the title, drop every character that is not a letter, "
                "digit, space or hyphen, and join the remaining words with single hyphens",
                ['slugify("Hello, World!") == "hello-world"', 'slugify("Data Science 101") == "data-science-101"'],
                ['slugify("  Many   spaces  ") == "many-spaces"', 'slugify("C\'est la vie") == "cest-la-vie"',
                 'slugify("already-a-slug") == "already-a-slug"']),
    "parse_duration": ("parse_duration(text: str) -> int: convert a duration such as '1h30m' or '45s' into seconds; "
                       "units are h, m and s; raise ValueError for anything else",
                       ['parse_duration("1h30m") == 5400', 'parse_duration("45s") == 45'],
                       ['parse_duration("1h2m3s") == 3723', 'parse_duration("2h") == 7200',
                        'raises(ValueError, parse_duration, "abc")']),
    "merge_intervals": ("merge_intervals(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]: merge "
                        "overlapping or touching intervals and return them sorted",
                        ['merge_intervals([(1, 3), (2, 6), (8, 10)]) == [(1, 6), (8, 10)]'],
                        ['merge_intervals([(1, 2), (2, 3)]) == [(1, 3)]', 'merge_intervals([]) == []',
                         'merge_intervals([(5, 7), (1, 2)]) == [(1, 2), (5, 7)]']),
    "roman_to_int": ("roman_to_int(numeral: str) -> int: convert a Roman numeral to an integer",
                     ['roman_to_int("XIV") == 14'],
                     ['roman_to_int("MCMXCIV") == 1994', 'roman_to_int("IX") == 9', 'roman_to_int("LVIII") == 58']),
    "top_words": ("top_words(text: str, n: int) -> list[tuple[str, int]]: the n most frequent lower-case words "
                  "with their counts, most frequent first, ties broken alphabetically",
                  ['top_words("a b b c c c", 2) == [("c", 3), ("b", 2)]'],
                  ['top_words("x y z y z", 3) == [("y", 2), ("z", 2), ("x", 1)]', 'top_words("", 2) == []',
                   'top_words("A a b", 1) == [("a", 2)]']),
    "is_valid_isbn10": ("is_valid_isbn10(code: str) -> bool: check an ISBN-10, ignoring hyphens; the last "
                        "character may be X meaning 10",
                        ['is_valid_isbn10("0306406152") is True', 'is_valid_isbn10("0306406153") is False'],
                        ['is_valid_isbn10("0-306-40615-2") is True', 'is_valid_isbn10("080442957X") is True',
                         'is_valid_isbn10("12345") is False']),
}


FENCE = "`" * 3      # the Markdown code fence the model wraps its code in


def ask(prompt):
    body = {"model": "llama3.2", "stream": False, "options": {"temperature": 0, "seed": 42, "num_predict": 400},
            "messages": [{"role": "user", "content": prompt}]}
    req = urllib.request.Request("http://localhost:11434/api/chat", json.dumps(body).encode(),
                                 {"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=900) as r:
        reply = json.load(r)["message"]["content"]
    block = re.search(FENCE + r"(?:python)?\n(.*?)" + FENCE, reply, re.S)
    return block.group(1) if block else reply


def run_tests(code, tests):
    # run the generated code in a separate process; report each failing assertion and why
    harness = code + "\n\ndef raises(err, fn, *args):\n    try:\n        fn(*args)\n    except err:\n        return True\n    return False\n"
    harness += "\nimport json\nfailed = []\n" + "".join(
        f"try:\n    assert {t}\nexcept Exception as e:\n    failed.append([{t!r}, type(e).__name__])\n" for t in tests)
    harness += "print(json.dumps(failed))\n"
    try:
        out = subprocess.run([sys.executable, "-c", harness], capture_output=True, text=True, timeout=20)
        return json.loads(out.stdout.strip().splitlines()[-1])
    except Exception:
        return [[t, "did not run"] for t in tests]


labels = ["description only", "with examples", "after one repair"]
totals = dict.fromkeys(labels, 0)
print(f"{'function':<17}" + "".join(f"{label:>19}" for label in labels) + "   (tests passed)")
shown_repair = None
for name, (spec, examples, hidden) in TASKS.items():
    tests = examples + hidden
    results = []
    for shown in ([], examples):
        prompt = f"Write a Python function {spec}.\n"
        if shown:
            prompt += "It must pass these examples:\n" + "\n".join(f"assert {t}" for t in shown) + "\n"
        prompt += "Reply with the code only, in one python code block."
        code = ask(prompt)
        results.append(run_tests(code, tests))
    # the coding-agent loop: show the model which tests failed, and ask for a fix
    failed = results[-1]
    if failed:
        report = "\n".join(f"assert {t}   -> {err}" for t, err in failed)
        code = ask(f"This Python function fails some tests.\n\n{FENCE}python\n{code}\n{FENCE}\n\nFailing tests:\n{report}\n\n"
                   "Fix the function so all tests pass. Reply with the code only, in one python code block.")
        results.append(run_tests(code, tests))
        if shown_repair is None:
            shown_repair = (name, report, results[-1])
    else:
        results.append(failed)
    for label, failed_tests in zip(labels, results):
        totals[label] += len(tests) - len(failed_tests)
    print(f"{name:<17}" + "".join(f"{f'{len(tests) - len(r)} of {len(tests)}':>19}" for r in results))

n_tests = sum(len(e) + len(h) for _, e, h in TASKS.values())
print(f"{'total':<17}" + "".join(f"{f'{totals[label]} of {n_tests}':>19}" for label in labels))

if shown_repair:
    name, report, still = shown_repair
    print(f"\nfeedback sent for {name}:\n{report}")
    print("still failing after the repair:", [t for t, _ in still] or "none")
text
function            description only      with examples   after one repair   (tests passed)
slugify                       5 of 5             5 of 5             5 of 5
parse_duration                1 of 5             1 of 5             1 of 5
merge_intervals               3 of 4             3 of 4             4 of 4
roman_to_int                  4 of 4             4 of 4             4 of 4
top_words                     4 of 4             4 of 4             4 of 4
is_valid_isbn10               2 of 5             2 of 5             2 of 5
total                       19 of 27           19 of 27           20 of 27

feedback sent for parse_duration:
assert parse_duration("1h30m") == 5400   -> ValueError
assert parse_duration("45s") == 45   -> ValueError
assert parse_duration("1h2m3s") == 3723   -> ValueError
assert parse_duration("2h") == 7200   -> ValueError
still failing after the repair: ['parse_duration("1h30m") == 5400', 'parse_duration("45s") == 45', 'parse_duration("1h2m3s") == 3723', 'parse_duration("2h") == 7200']

What the output shows

  • Plausible is not correct. The model passed 19 of 27 tests. Its failing parse_duration came with a tidy docstring and comments, and could not parse "1h30m" at all, because it split the text on spaces.
  • Examples in the prompt changed nothing here: 19 of 27 again. Showing a model what right looks like does not make it check its work.
  • Feedback fixed one function. Told which merge_intervals test failed, the model corrected the edge case of touching intervals.
  • It did not fix the hardest one. Given four failing parse_duration tests, all raising ValueError, it still produced code that failed every one. Feedback helps when the model is close; it does not rescue a wrong approach.
  • Only the tests revealed any of this. Read quickly, every function looked finished.

Larger models pass far more tests than a 3-billion-parameter model on a laptop. The pattern still holds: output that looks done, a gain from running tests and feeding back failures, and failures that need a person to rethink the approach.

How to use assistants well

Give the assistant a check it can run. Anthropic's guidance for its coding agent puts this first: give it tests, a build or a screenshot comparison, so it can verify its own work instead of stopping when the code "looks done" (Anthropic: Claude Code best practices). Write or approve the tests yourself.

Run the loop: generate, test, feed back, repeat. That is what coding agents automate. Cap the rounds. If two repairs fail, step in and change the approach.

Manage context. The same guidance names the context window as the most important resource: performance degrades as it fills with files, logs and conversation. Start fresh sessions for new tasks and point the assistant at the specific files that matter.

Review like a pull request from a new colleague. Check edge cases, error handling, input validation and anything touching security, secrets or data.

Use it where it is strongest: boilerplate, tests, explanations of unfamiliar code, refactoring with good test coverage, and first drafts you will review.

Measure your own speed honestly. If a task would take you five minutes, a long back-and-forth with an assistant may cost more. The METR result is a reminder to check rather than assume.

For teams

  • Agree what may be shared. Proprietary code and secrets should go only to tools approved for them.
  • Keep the review bar unchanged for AI-written code.
  • Invest in tests and CI. They are what make assistant output safe to accept.
  • Watch dependencies. Assistants can suggest packages that do not exist, which attackers can register under the invented names. Across 16 models, one study found at least 5.2% of recommended packages hallucinated for commercial models and 21.7% for open-source ones (Spracklen et al.: package hallucinations).

For the loop behind coding agents, see AI agents explained. For the tool-calling mechanism they use, see function calling and tool use. For a broader view of the AI tools developers use, see generative AI tools for developers.

Common mistakes

  • Accepting code because it reads well.
  • No tests, so the assistant cannot check itself and neither can you.
  • Endless repair loops on an approach that is wrong.
  • Pasting secrets or proprietary code into unapproved tools.
  • Letting a session's context grow until the assistant loses track.
  • Assuming you are faster without measuring.

Do AI coding assistants make developers faster?

Sometimes, not always. In a randomised trial with experienced developers working in their own large projects, allowing AI tools made tasks take 19% longer, though the developers believed they were faster. Gains tend to be larger on boilerplate and unfamiliar code.

Is AI-generated code secure?

Not by default. A study of GitHub Copilot found about 40% of programs generated for security-relevant scenarios vulnerable. Review AI-written code like any other contribution, with tests, static analysis and attention to input handling and secrets.

How do I get better code from an AI assistant?

Give it a precise task, the relevant files and a way to check its work, such as tests it can run. Then iterate on failures, and take over when repeated attempts do not converge.

Should I let an AI agent run tests and fix code on its own?

Yes, within limits: a sandboxed environment, tests you trust, a cap on attempts, and human review before merging. The test-and-repair loop is where agents add the most value.

Writing and testing Python for AI applications, including with AI assistants, runs through the Generative AI course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Generative AI?

Join GloryTecks’ expert-led Generative AI training in Hyderabad with real-time projects and 100% placement support.

Explore course