Code Critique: Rich, Immediate Critiques of Coding Antipatterns

A Catalog of Patterns & Antipatterns

These antipatterns are not the classic design antipatterns seen in industry, but rather unique to beginners. By understanding common antipatterns, instructors can preemptively address misconceptions, and students can learn to avoid these pitfalls. Conversely, by emphasizing good patterns, we reinforce sound design principles early on. Below we present a catalog of several common patterns and antipatterns observed in novice code, especially in the context of introductory programming courses. Each entry includes the Name (and whether it is a Pattern or Antipattern), the Intent or context of its occurrence, the Symptoms (how to detect it in code), the Consequences (impact of the pattern), an Example drawn from student code, and Repair or Mitigation strategies (how to fix an antipattern, or in the case of a positive pattern, how to apply it or its benefits).

Parameterized Function (Pattern)

Intent: Encapsulate a specific computation or algorithm in a reusable function that accepts parameters, instead of hard-coding values or writing one-off code for each scenario. Novice programmers use this pattern to avoid duplication and magic numbers by generalizing their solution. For example, if students need to compute a mathematical function (like a derivative) for different inputs, the parameterized function pattern suggests writing a function that takes the function and the input as parameters, rather than computing it only for one case.

Symptoms: (Characteristics of having applied this pattern) The code contains a function definition with parameters that represent varying inputs or configurations. The function’s body uses these parameters to carry out a computation, and the program calls the function with different arguments to handle multiple cases. You will not see repeated code for each case; instead, the logic is centralized in the function. For instance, a parameterized derivative function might accept any functionf(x) and step size h to compute a numerical derivative, instead of using fixed values inside.

Consequences: This pattern promotes modularity, clarity, and reusability. The function can be reused with different data, making the code easier to extend to new requirements. It improves maintainability since changes to the algorithm (or bug fixes) only need to be made in one place. The code’s purpose is clearer because the function name and parameters serve as documentation (e.g., computeDerivative(func, x, h) is self-explanatory about what it does). Overall, using parameterized functions helps novices avoid brittle, hard-wired code and encourages thinking in terms of general solutions.

Example: Below is an example of the Parameterized Function pattern in MATLAB. We define a function to compute a derivative given any function handle func, an evaluation point x, and a step size h. This allows calculating derivatives for different functions and values by calling computeDerivative with appropriate arguments, instead of writing separate code for each case.


% Pattern: Parameterized Function
function derivative = computeDerivative(func, x, h)
    % Computes the numerical derivative of func at x using central difference
    derivative = (func(x + h) - func(x - h)) / (2 * h);
end
						

Example usage of the pattern:

 
f = @(t) sin(t);
x_val = pi / 4;
step = 0.001;
deriv_val = computeDerivative(f, x_val, step);
disp(["Derivative at x=" num2str(x_val) ": " num2str(deriv_val)]);
						

In this example, computeDerivative is a general solution. We can reuse it for any f, any x, and any h. The pattern makes the code clean and adaptable – if we later need to compute a derivative of another function or at another point, we can do so by calling the function with new parameters, without modifying the function’s internal code. This stands in contrast to writing the derivative computation inline with specific values (which would be an instance of the Magic Number antipattern, as we’ll see below). 

Repair/Mitigation: N/A. (This is a good pattern. To “apply” this pattern, identify code that is duplicated or overly specific, and refactor it into a parameterized function. The mitigation for not using this pattern is essentially to refactor code to use it – which is discussed as the solution in the Magic Number antipattern below.)

Magic Number (Antipattern)

Intent: To use a quick, literal value in code to achieve a result, without explaining the value or making it configurable. Novice programmers often fall into this when they need a constant or a “secret” value – they just write the number (or string) directly in the code wherever needed. This antipattern commonly appears when calculating formulas or setting loop bounds, e.g., using a raw number like 0.001 or 37 in the code because it “seems to work” for the given problem.

Symptoms: Unnamed constant values appear directly in the source code (other than trivial cases like 0 or 1 in obvious contexts). These values may repeat in multiple places or be related in hidden ways. There is no comment or constant definition explaining their meaning. For example, you might see code like if (x > 37) ... with no explanation of why 37 is a special threshold, or a calculation y = x * 1.8 + 32 (where 1.8 and 32 are magic numbers for Fahrenheit conversion, ideally should be named). In MATLAB, a student computing a derivative might write something like h = 0.001; ... result = (f(x+h) - f(x-h)) / (2*h); using 0.001 (and maybe another occurrence of 0.00001 elsewhere) directly in the formula. These unexplained literals are the hallmark of Magic Numbers.

Consequences: The Magic Number antipattern leads to brittle, unclear, and unmaintainable code. Key consequences include:

  • Lack of Clarity: The purpose or meaning of the numeric value is unclear to someone reading the code (or even to the author, a week later). For instance, why 37? Is it degrees Celsius, some array size, a special code? Magic numbers obscure intent.
  • Reduced Maintainability: If the number needs to change later (say the formula constant needs tweaking), the programmer must find every instance of it and update them consistently. This is error-prone – one missed spot can introduce bugs. It’s also hard for someone else to safely modify the code without understanding what that number represents.
  • Reduced Reusability: The code is tightly coupled to that specific value. It can’t be reused in a different scenario where a different constant is needed without editing the code. In our derivative example, if we wanted a different step size, we’d have to edit the source – whereas a parameterized approach would let us call the function with a new h.
  • Increased Error-Proneness: If a magic number is mistyped or inconsistently updated, it can introduce subtle bugs. Also, using the wrong value (due to a misunderstanding) can go unnoticed because nothing in the code flags it as unusual – it just silently computes wrong results or suboptimal behavior.

Overall, magic numbers violate fundamental software engineering principles of clarity and DRY (Don’t Repeat Yourself), and in an educational context, they reflect a superficial understanding (the student found a number that “makes the output right” but doesn’t understand why).

Example: Consider a MATLAB script where a student is computing numerical derivatives at certain points:


% Antipattern: Magic Numbers
f = @(x) sin(x);
x_val = pi/4;
h = 0.001; % step size (magic number used directly)
deriv_val = (f(x_val + h) - f(x_val - h)) / (2 * h);
disp(["Derivative at x=" num2str(x_val) ": " num2str(deriv_val)]);

f2 = @(t) t.^2;
t_val = 2;
h2 = 0.00001; % another magic number
deriv_val2 = (f2(t_val + h2) - f2(t_val - h2)) / (2 * h2);
disp(["Derivative at t=" num2str(t_val) ": " num2str(deriv_val2)]);
							

In this code, the values 0.001 and 0.00001 appear out of nowhere. The comments show that the student might vaguely recognize them as “step size,” but they are still magic numbers – if someone else looks at this, they might ask: Why 0.001? Why not 1e-6 or some other step? What is special about these values? There’s no documentation or parameter name to clarify. If later the student decides to refine the accuracy, they must hunt down both occurrences of 0.001 and 0.00001 and change them appropriately. If they forget one, the code’s two derivative calculations might behave inconsistently.

Repair/Mitigation: Replace magic literals with named constants or parameters. One solution is to define a constant (e.g., STEP_SIZE = 0.001;) and use STEP_SIZE in the code – at least this gives the value a name indicating its role. A better solution, especially for cases like a derivative calculation needed for multiple functions, is to apply the Parameterized Function pattern: write a function (e.g., computeDerivative) that takes the step size (and any other necessary data) as an input. In our example, we could have a function computeDerivative(f, x, h) and call it with h = 0.001 or any other value. This not only removes the magic number from the implementation, but also makes it easy to change or choose different step sizes per use case. In short: expose the constant as a named quantity – through a variable, constant definition, or function parameter – and if the same logic is used in multiple places, refactor it into a single function. By doing so, the code becomes self-documenting and safer to modify.

Empty Loop (Antipattern)

Intent: Include a loop or other control structure in the program without a real need, often as an automatic or knee-jerk reaction to having just learned that construct. Novice students sometimes believe that they must use a recently taught concept in their solution even if the problem doesn’t require it. This antipattern is common right after loops are introduced – a student writes a loop that performs no meaningful work, just to follow a perceived instruction of “use a loop.”

Symptoms: A loop (for-loop or while-loop) whose body either does nothing or does work that has no effect on the overall program results. In code, you might see an empty loop block { } or end with no statements inside, possibly with a comment like “% loop serves no purpose.” Or the loop body may execute some statements that don’t influence the program (e.g., printing a loop index that isn’t used elsewhere, or computing a value that is never used after the loop). Essentially, if you can remove the entire loop and the program’s observable behavior doesn’t change, it was an unnecessary loop. This pattern is easily noticed when scanning code: the loop stands out but you realize it’s not connected to other logic.

Consequences: The immediate consequence is wasted effort – the loop needlessly uses computation cycles (though for small loops this is minor, it’s the principle). More importantly, it adds confusion for the reader (including the student themselves). Someone maintaining or grading the code might spend time trying to figure out the purpose of the loop, only to realize it has none. It can hide the real structure of the solution by adding noise. In some cases, an empty or irrelevant loop could even lead to performance issues (imagine an unnecessary loop with a million iterations doing nothing – it wastes time). In extreme cases, if the loop was meant to do something but is left empty, it could indicate a logical error (the student perhaps intended to put code inside but forgot). Overall, this antipattern indicates a mismatch between the code and the problem’s needs, often stemming from misunderstanding the concept’s appropriate use.

Example: A MATLAB example demonstrates this antipattern clearly. Suppose students just learned about loops and feel they should use one in every program. One student writes:


A = [3 6 9 4 1];
% Loop serves no purpose.
for i = 1:length(A)
end
disp("Sum of A is " + num2str(sum(A)));
							

Here the loop iterates over all elements of A but does absolutely nothing in each iteration. The student’s actual goal was probably to compute something (maybe sum the array), but since MATLAB has a built-in sum(A), there was no need for a loop. The student still wrote a for loop, possibly thinking “we just covered loops, so I must include one in my solution”. The comment even admits the loop is purposeless. This is a textbook Empty Loop antipattern. Another variant in Java might be:


// Java example of knee-jerk loop
int x = 5;
for (int i = 0; i < 10; i++) {
   // using a loop even though we only needed one check
   if (x < 0) { 
      // (body intentionally left blank)
   }
}
System.out.println(x);
							

In this contrived snippet, the loop runs 10 times doing nothing meaningful; perhaps the student thought they needed a loop to check a condition, when a simple if would suffice.

Repair/Mitigation: Remove or repurpose the unnecessary structure. If a loop is truly not contributing to the result, delete it. In cases where the loop was added out of habit, you should ask, “What was I intending to use this loop for? Can the same task be done without it?” Often, the fix is to replace the empty loop with nothing at all (if it had no effect), or to replace a convoluted loop+condition structure with a single straightforward construct. Educators can mitigate this by clarifying to students when a loop is needed (e.g., “only use a loop if you have repetitive work to do or need to iterate over data”). If the loop was included because the student thought every function needed one, reassure them that it’s fine to have a function without a loop if the problem doesn’t call for repetition. In an automated critique scenario, highlighting an empty loop and asking the student “Why is this loop here? What does it accomplish?” can prompt reflection. The student should either justify it or realize it’s superfluous. In summary, the cure is conscious coding: don’t add constructs unless they serve a purpose, and if you see one in your code that doesn’t, remove it to simplify the program.

Misplaced Code (Antipattern)

Intent: Write code outside the proper structural blocks of a program, typically by placing executable statements at the class or global level where they don’t belong. Novice Java (or C#) students often do this when they haven’t yet internalized that all logic must be inside methods/functions. They might start writing calculations or loops as soon as they open a { for a class, forgetting to wrap them in a main method or another function. The intent (from the student’s view) is simply to write the program’s steps, but the misplacement means the code is not in a valid location.

Symptoms: In languages that require methods, you will find statements (variable declarations, assignments, loops, etc.) directly inside a class, but not inside any method. The code may look logically correct at first glance, but it violates the language’s structural rules. For example, you might see:


public class Hello {
   String s = "Hello, World";
   for (int i = 0; i < s.length(); i++) { // <-- this loop is outside of any
method!
      result = s.charAt(i) + result;
   }
   return result;
}
							

This is a Misplaced Code antipattern: the loop and return statement are written in the class body where only declarations are allowed, not execution logic. The symptom is a compile error (in Java) complaining about code outside a method, but to the novice the error might be confusing (“illegal start of expression” or similar). Another symptom is that the student’s code lacks a main method or any method, yet tries to perform computations. Essentially, if you see return at the class level or standalone statements in between method definitions, you’ve spotted this antipattern.

In languages like Python (which allow top-level code in scripts), “misplaced code” might appear as code that should be inside a function (for reuse or clarity) but isn’t. However, the most clear-cut cases are in strictly structured languages (Java, C++) where it’s a syntax error.

Consequences: The immediate consequence is that the program won’t compile or run (for compiled languages). The student will get an error and often be baffled because “the code itself” (the algorithm) might be correct, but the placement is wrong. This antipattern thus prevents the code from executing at all, blocking progress until fixed. The compiler errors for this situation are notoriously not newbie-friendly – e.g., “class, interface, or enum expected” – which doesn’t clearly tell the student “you need to put that code inside a method.” So novices can get frustrated, not understanding why their straightforward loop is invalid. Beyond compilation, the presence of misplaced code indicates the student hasn’t grasped program structure, which could lead to further design problems as programs grow. It also hints that they are thinking in a purely linear script mindset, ignoring required boilerplate (like a main method or class design).

Example: The Java snippet above is a perfect illustration. The student likely intended to reverse a string (the loop builds result by prepending characters from s), and perhaps they assumed writing those lines in the class would execute them. The correct approach would be to put those lines inside a method, for example:


public String reverseString() {
   String s = "Hello, World";
   String result = "";
   for (int i = 0; i < s.length(); i++) {
      result = s.charAt(i) + result;
   }
   return result;
}
							

But in the antipattern version, they omitted the method header. WebTA’s critiquing logic is actually able to detect this situation by parsing the code and bundling the stray statements as a unit; it then realizes those should be inside a method and gives feedback to the student. A human instructor, upon seeing the code, would immediately say “Oops, you need to put that code inside a method” – a recognition of the Misplaced Code antipattern.

Repair/Mitigation: The fix is straightforward: move the code into an appropriate method or block. In Java, that means wrapping the logic in a public static void main(String[] args) (if it’s the main program) or another helper method. In general, ensure all executable statements are inside a procedure/function. To help students avoid this, instructors should emphasize the skeleton that every program needs (for Java, a class with methods; for C, inside main; etc.). When this error happens, an effective feedback (automated or manual) is: “Your code statements follow the rules of the language, but they’re not in the right place. In Java, all statements must belong to a method; move these inside a method.” Once the student places the code inside a method, the antipattern is resolved. As a preventive measure, some instructors provide a template file with the class and method already set up, so students fill in their logic in the right spot, reducing the chance of this mistake. Over time, as students get used to the structure, this antipattern vanishes.

Pseudo-Implementation (Antipattern)

Intent: Implement the behavior of a required interface or abstract class without actually linking it in the class definition – essentially “pretending” to implement an interface by writing the methods, but never using the implements keyword (or extends for an abstract base). Novices stumble into this when they fulfill the method requirements on their own class but forget (or don’t understand how) to declare the formal relationship. The intent from the student’s perspective is to satisfy the assignment (e.g., provide all the required functions), but they omit the formal contract.

Symptoms: A class contains methods that match an interface’s specification, but the class definition does not declare that it implements the interface. For example, suppose there is an interface ReverseInterface with a method reverseString(String). A student writes a class Reverse that has a public String reverseString(String s) method with the correct logic, but the class signature is just public class Reverse { ... } with no implements ReverseInterface. This is a Pseudo-Implementation antipattern. Another variant is not extending an abstract class while implementing the required methods, or only partially implementing required methods. Symptoms include: the program might compile (if no one actually tries to treat Reverse as a ReverseInterface), but fails in a context where the interface is expected. For instance, in instructor tests that do ReverseInterface obj = new Reverse();, the student’s class will not be considered a subtype, causing a type error or test failure. In some cases, the student’s code might work in their own tests because they always use Reverse directly, hiding the issue.

Consequences: This antipattern can be vexing and hard to debug for the student. If the interface or abstract class contract isn’t honored, any code that relies on polymorphism will break. The student might pass all functionality tests (since their method works in isolation) but then get a zero on an interface implementation requirement because their class isn’t recognized as implementing it. It reveals a misunderstanding of object-oriented concepts. Consequences include: failing instructor or auto-grader tests that expect proper inheritance, and learning the wrong lesson about interfaces (the student might think “well, if it works, why need the implements clause?”). Additionally, if not caught early, this could lead to code duplication or design issues – for example, if they don’t formally extend a base class, they might rewrite code that could have been inherited. In a teaching context, the biggest consequence is missing out on the benefits of abstraction: the whole point of the interface was to allow polymorphic use, which the student’s code nullified by not actually implementing it.

Example: Using the interface example above:


public interface ReverseInterface {
   public String reverseString(String s);
}

// Student"s class (antipattern: forgets to implement the interface)
public class Reverse {  // should be "implements ReverseInterface"
   // required by ReverseInterface:
   public String reverseString(String s) {
      String result = "";
	  for(int i = 0; i < s.length(); i++) {
	     result = s.charAt(i) + result;
	  }
	  return result;
   }
}
							

The Reverse class provides the reverseString method as specified, but nowhere does it say implements ReverseInterface. A savvy instructor or code reviewer will notice this immediately. An automated test might do something like:


// Test to detect the Pseudo-Implementation pattern 
ReverseInterface ri = new Reverse();
ri.reverseString("hello");
							

If the student has the antipattern, the above line won’t even compile (because Reverse is not a ReverseInterface. The student, however, might only test Reverse by calling its method directly (new Reverse().reverseString("hello"), which works and thus they think all is well. This discrepancy between “seems to work” and “not actually correct OOP-wise” is exactly what makes it an antipattern.

Repair/Mitigation: The fix is to add the missing implements/extends declaration – e.g.,


public class Reverse implements ReverseInterface
							

. This enforces the contract and lets the class be used polymorphically as intended. In cases where only a subset of methods were implemented (perhaps the student didn’t realize some were abstract or needed), the fix is to implement all required methods or declare the class abstract if appropriate. Mitigation through instruction: emphasize to students that implementing an interface is a two-step process – declare it in the class definition and implement all its methods. For automated critique, one strategy is to detect this pattern either statically (by checking the class’s implemented interfaces against its method signatures) or dynamically (as shown in the test above, try to cast or use as interface). When detected, the feedback should be gentle: “It looks like you wrote all the required methods for InterfaceX, but you didn’t declare that your class implements InterfaceX. Be sure to add implements InterfaceX to your class definition so that it fulfills the contract.” Once the student does so, their code becomes correct and robust. This antipattern is a good teachable moment about why syntax (like the implements keyword) matters for the program’s design, not just the compiler’s picky rules.

Localized Instance Variable (Antipattern)

Intent: Declare an instance (or global) variable that is actually used only locally within one method – effectively giving a variable a broader scope than necessary. Novice programmers might do this because they aren’t comfortable with passing variables around or they mistakenly think it needs to be a field to be accessible in a method. The intent is usually to hold some value, but the student doesn’t realize it could simply be a local variable inside the method.

Symptoms: A class has a field (instance variable) that is only referenced inside a single method. Outside that method, the variable is never read or modified. Often, the variable is initialized at declaration or in the constructor, then immediately reused in one method’s computation. For example:


public class Calculator {
   private int sum;  // instance variable
   public int add(int a, int b) {
      sum = a + b;
	  return sum;
   }
}
								

Here sum doesn’t need to persist beyond the add call; it’s effectively a temporary. A more novice-looking example:


public class Reverse {
   String result = ""; // instance variable, intended for accumulation
   public String reverseString(String s) {
      for(int i = 0; i < s.length(); i++) {
	     result = s.charAt(i) + result;
	  }
	  return result;
   }
}
								

In this case, result is an instance variable but is only used within reverseString. There’s no need for result to be a field – it doesn’t maintain state between multiple calls or across different methods. The symptom is clear: if you “slice” the code to see usage of result, it’s confined entirely to one method. Also, if the variable is not public and not used outside, it points to this antipattern (if public, one could argue maybe it’s meant for external access – but often novices leave it default or private anyway).

Consequences: Using an instance variable where a local would suffice can cause confusion and potential bugs. It increases the state held by the object unnecessarily – imagine if reverseString is called twice on the same object, the result field would retain its value from the first call, so the second call might start with result not empty (unless the method resets it). This could cause incorrect results in some scenarios (e.g., if the student forgets to reset result = "" at the start of the method, calling reverseString twice would produce a wrong answer the second time). Even if it doesn’t cause an immediate bug, it makes the class design less clean: the class carries baggage (the field) that isn’t conceptually part of its long-term state. It also makes the method impure in a sense – it relies on and modifies object state, which isn’t needed. In terms of pedagogy, it indicates the student might not fully understand variable scope or is overusing fields. The program would be easier to reason about if result were just a local variable within the method. This antipattern can also lead to threading issues (in advanced contexts) or unexpected side effects if someday the class is expanded. For novice learning, the main consequence is a missed opportunity to use proper scoping and a risk of weird behavior if the method is called multiple times or from multiple places.

Example: The Java Reverse class above shows the issue. Another simple example in a broader context:


public class Counter {
   int temp;  // intended only to be used in compute()
   public int compute(int[] data) {
      temp = 0;
	  for(int x : data) {
	     temp += x;
	  }
	  return temp;
   }
   // "temp" is not used in any other method
}
								

Clearly, temp could just be a local variable inside compute. As written, if compute is called in parallel on the same Counter object (in different threads, say), they’d interfere via the shared temp. That’s advanced, but it highlights why unnecessary sharing of state is bad. In a single-thread case, if someone accidentally reads counter.temp after calling compute, they might get a result, but that’s not part of the class’s intended interface – it’s like a leaky internal detail. All this from a novice simply not knowing or thinking to keep it local.

Repair/Mitigation: Narrow the scope of the variable to where it’s needed. The is usually to change the instance variable into a local variable within method. In the Reverse example, we would move String result = ""; inside the reverseString method (and remove it as a field). After that change, each call to reverseString has its own result variable, which is initialized fresh and not stored in the object. This makes the method self-contained and side-effect-free (with respect to object state). A general guideline to impart to students: only use fields for data that truly represents the persistent state or attributes of an object. If something is just an intermediate value in a calculation, keep it local. Tools or code reviews can detect this by slicing the abstract syntax tree or using static analysis to see variable scope. Our WebTA critiquer, for instance, flags instance variables that are written and read entirely within one method and nowhere else, suggesting to the student that they “make this variable local to the method”. The mitigation strategy is to always consider scope when declaring a variable: default to the smallest scope needed. If a student finds themselves creating a field, they should ask, “Will I need this value outside of the method that computes it?” If not, it shouldn’t be a field. By adopting that habit, this antipattern can be avoided altogether. In cases where an instance variable is intended to be used in multiple methods but currently isn’t, the instructor might question whether the design can be improved or simplified. But often with novices, it’s unintentional and the straightforward remedy is best.

Repeated Resource Instantiation (Antipattern)

Intent: Instantiate a resource (such as an I/O stream, scanner, database connection, etc.) multiple times unnecessarily, especially inside a loop, instead of reusing a single instance. Novices might do this because they’ve only seen the resource used in a simple context and they just copy that usage into a loop without understanding the cost or implications. For example, a common scenario is repeatedly creating a new Scanner for input on each iteration of a loop, rather than creating one Scanner and reusing it for all input.

Symptoms: Look for object creation (new SomeResource()) inside a loop or repeated block, where logically one instance would suffice outside the loop. In code, a classic case is:


for(int i = 0; i < 10; i++) {
   Scanner input = new Scanner(System.in);
   System.out.println("Enter number: ");
   int x = input.nextInt();
   ...
}
							

Here a new Scanner is made on every iteration. Another symptom in file I/O might be opening a file in each loop cycle. In MATLAB, an analogous mistake is re-reading the same file or re-opening a connection each time in a loop. The key sign is that the code performs an expensive setup operation repeatedly, often using identical parameters, where it could be done once. Sometimes the program still works, but with inefficiency; other times it can lead to only partial processing (e.g., reading only the first chunk repeatedly and discarding the rest). In our context, we observed some students instantiate objects like random number generators or network connections inside loops similarly. Repeated resource instantiation often coincides with another minor antipattern: Variable Declared in Loop (declaring a loop-local variable each time), which in some languages has performance implications.

Consequences: This antipattern can cause both performance issues and logical errors. Performance-wise, instantiating a resource repeatedly is often costly (e.g., opening a file 1000 times in a loop vs once). It also can hog system resources or lead to exhaustion (too many open connections). In the Scanner example above, repeatedly constructing a Scanner on System.in might work but is inefficient; if done incorrectly, it might even consume or skip inputs (for instance, if input is piped from a file, reopening Scanner could cause the later instances to find the stream already at end). Indeed, one subtle bug described in our research: when students opened a Scanner inside a loop to read from standard input piped from a file, the second instantiation effectively “reset” and lost some of the buffered input, causing the program to block or miss data. In terms of code quality, the intent “use one resource throughout” is obscured by the antipattern. It indicates the student thinks of each iteration in isolation, not realizing the cumulative effect or that setup could be done once. Another consequence is memory waste – although modern garbage collectors handle abandoned objects, it’s still poor practice. For novices, this antipattern hints at a lack of understanding of object lifecycle and scope beyond the simplest usage.

Example: Consider a simple task: read 10 numbers from the user and print their squares. A novice might write:


for (int i = 0; i < 10; i++) {
   Scanner input = new Scanner(System.in); // Antipattern: inside loop
   System.out.print("Enter a number: ");
   int num = input.nextInt();
   System.out.println("Square is " + (num*num));
}
							

Every iteration, a new Scanner is created. The correct approach is to create one Scanner before the loop, and use it for all inputs:


Scanner input = new Scanner(System.in);
for (int i = 0; i < 10; i++) {
    ...
	// use the same "input" for each iteration
}
							

Another example: a student needed to append to a string in a loop and wrote String result = new String(""); inside the loop each time, instead of once (though Strings are less costly, the idea is similar). In MATLAB, we’ve seen students do something like:


for i = 1:10
   fid = fopen("data.txt","r");
   line = fgetl(fid);
   fclose(fid);
end
							

Opening and closing the same file each loop is analogous to this antipattern. All these examples show a resource being unnecessarily recreated.

Consequences (recap): In the Scanner case, the code will prompt the user 10 times as expected. But if input were automated (piped), it might only actually process the first input and then stall (because the second Scanner starts reading from an already-consumed stream). Even without that, performance is poorer – constructing a Scanner does a lot of setup. The program is doing 10x the work needed. If this were a larger loop, the inefficiency scales. For file I/O, repeatedly opening/closing can drastically slow down execution. Also, if a resource has side effects (e.g., each new connection or object does some initialization), it could lead to unintended behavior each iteration.

Repair/Mitigation: Initialize resources outside loops and reuse them. The fix is conceptually simple: move the instantiation out of the loop. For the Scanner example, declare Scanner input = new Scanner(System.in); before the loop, then inside just use input.nextInt(). For file or database connections, open before, loop, then close after. In cases where a new object is needed each time (not common for the same resource), be sure it truly must be new (for example, reading different files each iteration is valid). Educators should stress best practices: e.g., “If you’re reading multiple values from the same input source, open that source once.” This antipattern is somewhat language-specific – for instance, in Java it’s idiomatic to reuse a scanner or stream, whereas in some other contexts short-lived objects are fine. But as a teaching point, it highlights considering the cost of object creation. Automated detectors in WebTA can catch this by pattern-matching a new call inside a loop construct. If found, the tool (or instructor) can warn: “You are creating a new [resource] each iteration. Consider moving this outside the loop to avoid unnecessary repetition.” Additionally, relating it to the concept learned: often this happens because students learn “to get input, do new Scanner(System.in)” in a simple scenario, and they just copy-paste that into a loop for multiple inputs. We can mitigate by explicitly teaching how to scale that example up: “Only one Scanner is needed for many inputs.” In summary, the repair is to declare once, use many – allocate resources once per needed scope, not per item processed. When fixed, the code is more efficient and clear in intent.

Crowded Operators (Antipattern)

Intent: Write expressions without proper spacing around operators, violating coding style guidelines and making code harder to read. Novices sometimes ignore stylistic conventions like spacing, either due to lack of knowledge or thinking it doesn’t matter since the code still works. The intent isn’t malicious – they are just focused on getting the code to run and may condense it or not realize the importance of readability. This antipattern’s intent could be described as “saving space or time by not typing spaces,” though often it’s simply negligence.

Symptoms: Expressions where binary operators (+, -, =, ==, etc.) have no whitespace around them, contrary to standard style. For example: x=5+3*y; instead of x = 5 + 3*y;. The code might compile and run perfectly, so the only symptom is visual clutter. In Python, PEP8 style guide specifically says there should be a space around operators (except higher precedence in mixed expressions). So a crowded operator example in Python would be x= c+b/a instead of the preferred x = c + b/a. Another symptom is when multiple operators are together and the spacing doesn’t indicate precedence, e.g., result= a*b +c;. While functionally the same as result = a*b + c;, the lack of space can slow down human parsing. Essentially, the code looks “cramped”. Some IDEs or linters will warn about this (e.g., “PEP8: E225 missing whitespace around operator”).

Consequences: Primarily, reduced code readability and maintainability. Code with crowded operators is harder to scan and understand at a glance. For instructors grading or peers collaborating, this increases cognitive load. Mistakes can hide in such dense code; for instance, if(x==y&&y< z) is slightly harder to read correctly than if (x == y && y < z). While experienced programmers can interpret both, enforcing spacing is about consistency and preventing minor misunderstandings. Also, lack of spacing can be symptomatic of a general disregard for coding standards, which could extend to other style issues (poor indentation, naming, etc.). In professional settings, not following style guidelines can lead to friction in teamwork, though in a classroom the main consequence is a lower code quality score or instructor comments. Importantly, teaching novices about this antipattern is teaching them that style matters – code is read more often than it’s written, and adhering to standard style (like spacing) is part of writing quality code. There are typically no functional bugs caused by this (except in languages where whitespace is semantic, but in those, it’s not just style then), so the impact is on human communication and long-term maintenance.

Example: A snippet demonstrating crowded operators in Python:


# Crowded operators antipattern
x=10+5  
# intended to assign 15 to x, but no spaces around = or +
y=x*2+3 
# no spaces around operators here either
if(y>20 and x<=15):
   print("Crowded operators example")
 							

PEP8 would recommend writing that as:


x = 10 + 5
y = x * 2 + 3
if (y > 20 and x <= 15):
   print("Proper spacing example")
   							

The crowded version works exactly the same, but it’s less readable – especially with y=x*2+3, one can mis-see it as y = x * 2 + 3 only after adding mental spacing. In Java/C-style syntax, you might see int z=a+b*c; versus int z = a + b * c;. The latter is clearer. Our project’s style-focused critique component identified crowded operators as a target antipattern for automated detection in student Python code.

Repair/Mitigation:Follow standard coding style by adding appropriate whitespace around operators. Instructors should set expectations (possibly via a style guide or an automatic linter in the development environment) that x = c + b/a is the correct formatting. If a tool like WebTA detects crowded operators, the feedback would simply be: “Insert spaces around operators for readability.” This is an easy fix: just add a space before and after each operator (with some nuance for unary operators or when multiple operators are in one expression, but basic rule covers most cases). Mitigation can be automated – many IDEs can auto-format code. But pedagogically, it’s good to explain why spacing matters: it makes the structure of expressions visible and conforms to what other programmers expect to see (so they can read your code effortlessly). Another mitigation strategy is to include style points or a style checker in the assignment workflow, so students get used to correcting these issues. In summary, to repair crowded operators: take a moment to format your code. The code’s logic doesn’t change, but its readability greatly improves. By instilling these habits early, students will carry them into future projects, making everyone’s life easier when collaborating or reviewing code.

Demonstration

PatternDB Demo: Browse source code patterns and antipatterns in our database. Launch Demo


NSF Logo This work was partly funded by the National Science Foundation awards #2142309 and #1504860.

Additional support was provided by Michigan Technological University, the Institute of Computing and Cybersystems, and a Jackson Blended Learning Grant through the William G. Jackson Center for Teaching and Learning.
MTU Logo ICC Logo