<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ladislav Szép</title>
    <description>The latest articles on DEV Community by Ladislav Szép (@neolix).</description>
    <link>https://dev.to/neolix</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4200535%2F8f65fb0c-9b15-45dd-afbd-cfb2476e8bb6.png</url>
      <title>DEV Community: Ladislav Szép</title>
      <link>https://dev.to/neolix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neolix"/>
    <language>en</language>
    <item>
      <title>You can't teach a variable with a maths problem</title>
      <dc:creator>Ladislav Szép</dc:creator>
      <pubDate>Mon, 26 Oct 2026 12:47:31 +0000</pubDate>
      <link>https://dev.to/neolix/you-cant-teach-a-variable-with-a-maths-problem-161k</link>
      <guid>https://dev.to/neolix/you-cant-teach-a-variable-with-a-maths-problem-161k</guid>
      <description>&lt;p&gt;I can write Python. I have shipped a web application in it, with a test suite and a deployment pipeline and arguments about coverage.&lt;/p&gt;

&lt;p&gt;I have also never finished a beginner programming course. Not one.&lt;/p&gt;

&lt;p&gt;Not because they were too hard. Because somewhere in the first week, every single one of them asked me to solve a maths problem before it had told me what a variable was.&lt;/p&gt;

&lt;p&gt;The genre&lt;br&gt;
You know the exercises. Every course has them, in roughly this order:&lt;/p&gt;

&lt;p&gt;print the numbers 1 to 100, but Fizz, but Buzz&lt;br&gt;
is this number prime&lt;br&gt;
the first n Fibonacci numbers&lt;br&gt;
a pyramid of hashes, n rows tall&lt;br&gt;
convert Celsius to Fahrenheit&lt;br&gt;
sum every even number below one thousand&lt;br&gt;
Look at what's actually being asked. The programming content of "is this number prime" is a loop and a conditional. The rest of it — why you only need to check up to the square root, why 1 isn't prime, what a factor is — is arithmetic that has nothing to do with Python and everything to do with whether you liked maths at school.&lt;/p&gt;

&lt;p&gt;Two difficulties, stacked, with no way to tell them apart&lt;br&gt;
When a learner fails that exercise, something has gone wrong. But which thing?&lt;/p&gt;

&lt;p&gt;They might not understand for. They might not understand modulo. They might understand both perfectly and have no idea what a prime number is for. From the outside these look identical: an empty editor and somebody who has stopped.&lt;/p&gt;

&lt;p&gt;And here is the part that actually costs the industry people. The learner diagnoses it themselves, immediately and wrongly:&lt;/p&gt;

&lt;p&gt;I was never any good at maths. I'm probably not cut out for this.&lt;/p&gt;

&lt;p&gt;That conclusion is false. Most professional Python is strings, dictionaries, HTTP and file handling. I have written far more dict.get than modulo in my life. But the learner has no way to know that yet, because the only programming they've been shown is maths, and they've just failed at it.&lt;/p&gt;

&lt;p&gt;They don't bounce off programming. They bounce off arithmetic wearing programming's coat, and they never find out the difference.&lt;/p&gt;

&lt;p&gt;So why does every course do it?&lt;br&gt;
This is the part I didn't expect, and it's the reason I'm writing this rather than just complaining.&lt;/p&gt;

&lt;p&gt;It isn't pedagogy. It's marking.&lt;/p&gt;

&lt;p&gt;A maths exercise has exactly one right answer. is_prime(7) returns True, and there is no argument about it. You can check that with one line of test code, and it will be correct for every learner, forever.&lt;/p&gt;

&lt;p&gt;Now try to auto-mark this instead:&lt;/p&gt;

&lt;p&gt;Print a shopping list with a total at the bottom.&lt;/p&gt;

&lt;p&gt;How many valid programs are there? Hundreds. Do you accept a trailing newline? Two spaces instead of one? "Total: 12" or "Total: 12.00"? A dictionary or two parallel lists? A function or straight-line code?&lt;/p&gt;

&lt;p&gt;Every one of those is a correct answer from a beginner, and a naive string comparison fails most of them. The learner then gets marked wrong for being right, which is worse than being bored.&lt;/p&gt;

&lt;p&gt;Maths exercises aren't in your first week because they teach well. They're in your first week because they're cheap to grade. It is a testing constraint that leaked into the curriculum, and it has been there so long that everyone mistook it for teaching.&lt;/p&gt;

&lt;p&gt;What the alternative actually costs&lt;br&gt;
I've been building a course on the other side of that trade, and the engineering is where all the cost lives.&lt;/p&gt;

&lt;p&gt;Three rules fell out of it, and the third surprised me.&lt;/p&gt;

&lt;p&gt;Checks are authored, not generated. Every check carries a sentence a human wrote about what was expected, in the lesson's own vocabulary. No "AssertionError: expected 'Total: 12' but got 'Total: 12.0'".&lt;/p&gt;

&lt;p&gt;A check may show what it saw, never what it is. This is the one I had to write down in the code, because it is very tempting to break:&lt;/p&gt;

&lt;p&gt;class CheckResult(BaseModel):&lt;br&gt;
    """One check's verdict, in the only terms the learner ever sees.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;`label` is authored by hand per check and `detail` carries observed
values. Check source never appears in either.
"""

label: str
passed: bool
detail: str | None = None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Show a learner the test that failed them and you have not taught them to solve the problem. You have taught them to satisfy the test, which is a different and much worse skill, and one that survives into their professional life.&lt;/p&gt;

&lt;p&gt;Several right answers must pass. This is the expensive half. It is not a feature you add; it is a constraint on every single task you write. Some tasks get a hidden pytest case that imports what the learner wrote and calls it. Some normalise whitespace before comparing. Some can't be auto-marked at all and have to be rewritten until they can.&lt;/p&gt;

&lt;p&gt;That is slow. 135 lessons in, I can tell you the authoring is three or four times the work of writing the maths version. That cost is the real reason the maths version won.&lt;/p&gt;

&lt;p&gt;What goes in the first week instead&lt;br&gt;
Here's a whole lesson, near the very start:&lt;/p&gt;

&lt;p&gt;score = 10&lt;br&gt;
print("score: " + score)&lt;br&gt;
TypeError: can only concatenate str (not "int") to str&lt;br&gt;
That's it. That's the lesson.&lt;/p&gt;

&lt;p&gt;It teaches types, and it teaches them in the only way that sticks: the learner has already been bitten, so there is a reason to care about the answer. No arithmetic. No domain knowledge. Nothing to be bad at except the thing actually being taught.&lt;/p&gt;

&lt;p&gt;The rule I ended up with is that where a concept exists because something doesn't work, show the failure first. A sentence about types teaches almost nothing. A TypeError with your own name on it teaches it permanently.&lt;/p&gt;

&lt;p&gt;Where I'll concede&lt;br&gt;
Maths exercises are not useless. If your goal is a computer science degree, discrete maths is load-bearing and you should meet it early. Those courses are not wrong — they are university courses doing a university course's job. The mistake is recommending them as the universal first step to somebody who wants to build a thing.&lt;/p&gt;

&lt;p&gt;"But programming is maths." Logic, yes. Arithmetic, mostly no. I want a beginner to meet boolean logic in week one. I do not want them to meet modular arithmetic in week one, and those two have been bundled together for forty years.&lt;/p&gt;

&lt;p&gt;I have no retention data. None. I have a design argument, one learner, and 135 lessons. Anyone telling you their pedagogy is proven after a year is selling something. If this is wrong, it'll be wrong in a way that shows up in a year, not in this post.&lt;/p&gt;

&lt;p&gt;And the cost is real. If you are building a course and you take this advice, you are signing up for roughly four times the authoring work per lesson and a check design you will revisit repeatedly. I think it's worth it. I also understand completely why the industry decided otherwise.&lt;/p&gt;

&lt;p&gt;This is part of LexipawsPY, a free Python course I'm building. Every lesson is free and stays free — the whole course, the drills, the hints and the solutions. It's at py.lexipaws.eu if you want a look, though fair warning: it's still being written. 135 lessons today, through loops, lists and tuples, stopping just before dictionaries.&lt;/p&gt;

&lt;p&gt;If you learned to code fine through FizzBuzz and think I'm overthinking this, I'd genuinely like to hear it — I'm aware I might be generalising from one person who doesn't like maths.&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
      <category>showdev</category>
    </item>
    <item>
      <title>You can't quiz someone into writing tests</title>
      <dc:creator>Ladislav Szép</dc:creator>
      <pubDate>Sun, 25 Oct 2026 20:08:40 +0000</pubDate>
      <link>https://dev.to/neolix/you-cant-quiz-someone-into-writing-tests-2nne</link>
      <guid>https://dev.to/neolix/you-cant-quiz-someone-into-writing-tests-2nne</guid>
      <description>&lt;p&gt;Every course I've taken has a lesson about testing. It explains what a unit test is, has you write one into a box, marks it green, and moves on. I passed several of those lessons. I did not start writing tests.&lt;/p&gt;

&lt;p&gt;The same is true of git. There's a lesson on commit messages, usually with a multiple-choice question about the imperative mood. Nobody's commit history changes because of it.&lt;/p&gt;

&lt;p&gt;That's not the courses being lazy. It's that these aren't facts, they're habits, and a habit is visible in a year of work rather than in one answer. You can quiz somebody on what a conventional commit is. You can't quiz them into writing one on a Tuesday when they're tired.&lt;/p&gt;

&lt;p&gt;So when I built my own Python course, I left that lesson out and wrote something else instead: the course reads your repositories and grades what's already there.&lt;/p&gt;

&lt;p&gt;What it actually does&lt;br&gt;
There are seven probes and eleven awards between them. Each probe opens a repository you point it at, runs the tools that repository already uses, and reads the output. Awards pay experience, which is what the XP column below is.&lt;/p&gt;

&lt;p&gt;Probe   What it looks for   XP&lt;br&gt;
git commit subjects that follow a convention    40&lt;br&gt;
git branches named for their work   25&lt;br&gt;
lint    a tree your linter has nothing to say about 40&lt;br&gt;
lint    formatting nobody has to argue about    25&lt;br&gt;
tests   a test suite that exists at all 50&lt;br&gt;
tests   a suite that is actually green  75&lt;br&gt;
types   signatures that say what they mean  40&lt;br&gt;
docs    a README someone else could walk through    25&lt;br&gt;
docs    decisions written down where you can ask about them later   40&lt;br&gt;
ci  something other than you checking the work  30&lt;br&gt;
ci  a pipeline that stays green 45&lt;br&gt;
Totals put you on a ladder: Starting out, then Finding the habits at 100, Habits that hold at 250, Dependable at 500, Seasoned at 1000.&lt;/p&gt;

&lt;p&gt;Every award carries evidence — the actual number it saw — because an award you can't check is a sticker.&lt;/p&gt;

&lt;p&gt;The three problems that were actually interesting&lt;br&gt;
Writing the probes was the easy half. The hard half was deciding what a probe's answer means.&lt;/p&gt;

&lt;p&gt;"I found nothing" and "I couldn't look" are different answers&lt;br&gt;
The CI probe wants to know whether your last few pipeline runs were green. That needs the GitHub CLI installed, signed in, and a network.&lt;/p&gt;

&lt;p&gt;If gh isn't there, the probe could reasonably say "no green pipeline". That's a disaster: the first time you scan on a laptop without gh, the course quietly takes away an award you earned, for a pipeline that is still green.&lt;/p&gt;

&lt;p&gt;So a probe returns one of two things, and the difference is load-bearing:&lt;/p&gt;

&lt;p&gt;def evaluate(self, repo: Path) -&amp;gt; list[Award] | None:&lt;br&gt;
    """What this probe finds now, or None if it could not look.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The difference matters. An empty list means the repository
does not earn this, and the scan takes the award away. None
means the probe could not run at all, and the scan leaves
what was already held alone - losing an award because ruff
was not installed today would be a lie about the work.
"""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A list — possibly empty — means I looked, and this is what's there. None means I couldn't answer, and the scan leaves whatever was already held alone.&lt;/p&gt;

&lt;p&gt;It ended up as two probes rather than one, because "does a workflow file exist" can always be answered and "were the last runs green" often can't:&lt;/p&gt;

&lt;p&gt;Two probes rather than one, because they fail differently. [...] One probe for both would revoke a green pipeline every time gh was missing, which is the lie the None return exists to prevent.&lt;/p&gt;

&lt;p&gt;Awards have to be revocable, and that's uncomfortable&lt;br&gt;
The obvious design is that experience only goes up. It feels better, it's what games do, and it avoids an awkward conversation.&lt;/p&gt;

&lt;p&gt;It's also a lie with a delayed fuse. If you earn "a suite that is actually green" and then break your tests and leave them broken, a number that still says otherwise is wrong — and you're the one person guaranteed to find out.&lt;/p&gt;

&lt;p&gt;So a scan can take an award back:&lt;/p&gt;

&lt;p&gt;An award that survives the work that earned it being undone is a lie the learner will eventually notice.&lt;/p&gt;

&lt;p&gt;A probe can't do this, because a probe only sees now. Only the scan knows what was held before, so only the scan can remove it.&lt;/p&gt;

&lt;p&gt;Re-earning is not re-paying&lt;br&gt;
If running a scan twice paid twice, the meter would measure how often you run scans.&lt;/p&gt;

&lt;p&gt;So an award's id identifies the achievement, not the occasion. Scan again and the evidence is refreshed, so it describes your repository as it stands today — but the date you first earned it is left alone, and nothing is paid twice.&lt;/p&gt;

&lt;p&gt;The threshold is 80%, on purpose&lt;br&gt;
The git probe looks at your last 100 commit subjects and wants 80% of them to follow the convention. Not all of them.&lt;/p&gt;

&lt;p&gt;I went back and forth on this and settled it with a comment that is still in the file:&lt;/p&gt;

&lt;p&gt;An occasional "wip" is a human being, not a failure of craft.&lt;/p&gt;

&lt;p&gt;A tool that demands perfection to acknowledge a habit teaches you to perform for the tool. The same reasoning sets the floor at ten commits: below that there's no habit to judge, just a handful of commits.&lt;/p&gt;

&lt;p&gt;Why it runs on your machine and not on a server&lt;br&gt;
Here's the part I'd have got wrong if I'd built it the obvious way.&lt;/p&gt;

&lt;p&gt;Running these probes means running your test suite. On a hosted service, that is executing a stranger's code on my server on their say-so — the exact thing the project's sandboxing decisions exist to prevent. So the hosted version refuses:&lt;/p&gt;

&lt;p&gt;403 — The craft meter runs real test suites, so it is for a local install&lt;br&gt;
So it runs where your code already is: you run the application on your own machine and point it at your own repositories. Your code is never uploaded, because there is nowhere to upload it to. No token, no OAuth scope, no third party with read access to your private repos.&lt;/p&gt;

&lt;p&gt;One honest caveat, since this is the part people ask about: the local install isn't something you can go and get yet. The hosted course is live, but the craft meter is the half that needs the application on your own machine, and packaging that up is still ahead of me. If this is the bit you want, say so and I'll know it's worth doing sooner.&lt;/p&gt;

&lt;p&gt;I'd like to claim that as a privacy feature. Honestly, it's a consequence — I couldn't safely build it the other way, and the constraint produced the better design. That happens more often than I expect it to.&lt;/p&gt;

&lt;p&gt;What it doesn't do&lt;br&gt;
It doesn't read your code and tell you it's good. It reads what your own tools say about it. If your linter is configured to permit everything, a clean tree means very little, and the probe can't tell.&lt;/p&gt;

&lt;p&gt;It won't make you employable on its own. It measures habits an interviewer can also see in thirty seconds of scrolling your GitHub — which is rather the point. Those are the habits worth having, so they are the ones worth counting.&lt;/p&gt;

&lt;p&gt;And it is genuinely slow. It runs pytest over every repository you give it. That's not a bug to optimise away; it's what makes the number mean something.&lt;/p&gt;

&lt;p&gt;This is part of LexipawsPY, a free Python course I'm building. Every lesson is free and stays free — the whole course, the drills, the hints and the solutions. It's at &lt;a href="https://py.lexipaws.eu/" rel="noopener noreferrer"&gt;py.lexipaws.eu&lt;/a&gt; if you want a look, though fair warning: the beginner path is still being written. It runs to sixty-eight lessons today, through loops, lists and tuples, and stops just before dictionaries.&lt;/p&gt;

&lt;p&gt;If you've built something similar, or you think grading habits is a terrible idea, I'd like to hear it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>career</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
