Python fresher rounds test whether you can explain the basics in your own words, write small programs cleanly and talk honestly about what you built. Expect questions on slicing, list and dict methods, scope, truthiness, files and exceptions, a few short coding tasks, and your college or internship projects. It is written for final-year students, new graduates and anyone moving into a first Python job from an internship or a course. Read each sample answer, then say your own version out loud. For deeper topics like generators, decorators and the GIL, go to the main Python page.
Search all questions by round, difficulty and level, or save the ones you want to practice.
Interpreted: the standard interpreter compiles your file to bytecode and runs it straight away, with no separate build step.
Dynamic typing: types belong to objects, not to variable names, and are checked while the program runs.
Strong typing: Python still refuses to mix types silently, so "3" + 4 raises a TypeError.
“Interpreted means I don't run a separate compile step. When I run a file, the standard interpreter turns it into bytecode and executes that on its own virtual machine right away. The catch is that some mistakes, like a typo in a branch that rarely runs, only show up when that line actually executes. Dynamically typed means a variable is just a name pointing at an object, and the type lives on the object. So x can point to an int and later to a string, and nothing complains until I use it wrongly. But Python is also strongly typed: adding the string three to the number four raises a TypeError instead of guessing. That's why I lean on tests and type hints to catch mistakes early.”
Saying Python has no compile step at all, or that dynamic typing means Python converts types for you.
.pyc file, and where does it come from?break, continue and pass do? And what does an else block after a for loop mean?break: leaves the loop at once.
continue: skips the rest of this pass and moves to the next item.
pass: does nothing; it holds a place where Python needs a statement.
Loop else: runs only when the loop finished without hitting break.
“break stops the loop immediately and jumps past it. continue skips the rest of the current pass and goes to the next item. pass does nothing at all; I use it when Python needs a statement, like an empty function or class I'll fill in later. The loop else is the one people forget. It runs only if the loop ended normally, without a break. That makes it handy for searching: I loop over items, break when I find a match, and put the not-found handling in the else. It saves me from keeping a separate found flag. The same else works on a while loop too.”
def find_user(users, name):
for user in users:
if user == name:
print("found", user)
break
else:
print("not found")
Thinking pass works like continue, or that a loop else runs whenever an if inside the loop was false.
else block run if the list is empty?else?if __name__ == "__main__": do at the bottom of a script, and why do people write it?__name__: every module has one; it is "__main__" when the file is run directly.
On import: __name__ is the module's own name, so the guarded block is skipped.
Why: the file can be both a runnable script and an importable module without side effects.
“Every Python module gets a variable called __name__. When I run a file directly, Python sets it to the string __main__. When another file imports it, __name__ is the module's own name instead. So the code inside that if-block only runs when I launch the file as a script, not when someone imports it. That lets me keep reusable functions at the top and a small entry point at the bottom, like parsing arguments and calling main(). Without the guard, importing my file in a test would run the whole script. It also matters for multiprocessing on systems that start new processes by re-importing the main file, where a missing guard can make the program start processes over and over.”
def main():
print("running as a script")
if __name__ == "__main__":
main()
Saying it is required for a script to run at all, or that it makes the code run faster.
main() function instead of directly under the if?if statement, and when is if not x: the wrong check to write?Falsy values: None, False, zero of any number type, and empty strings, lists, tuples, dicts and sets.
Everything else: is truthy, including the string "0" and a list holding one empty item.
The trap: when 0 or an empty string is a valid value, check x is None instead.
“The falsy values are None, False, zero in any number type, and empty containers: an empty string, list, tuple, dict or set. Everything else is truthy, including the string with a zero in it, which catches people out. My own classes can decide too, through __bool__ or __len__. if not x: is fine when I just mean 'is this empty'. It's wrong when zero or an empty string is a real answer. Say a function returns a discount that could be zero, or None when there's no discount. If I write if not discount, a real zero discount gets treated as missing. There I write if discount is None so only the missing case is caught.”
Saying the string "0" or "False" is falsy, or using if not x for a value where zero is valid.
bool([[]]) return, and why?0.1 + 0.2 == 0.3 come out False?Slash: always gives a float, even for 6 / 3.
Floor division: rounds down toward negative infinity, so -7 // 2 is -4.
Modulo: the remainder takes the sign of the divisor, so minus seven modulo two is 1.
Floats: stored in binary, so 0.1 is only approximate; compare with math.isclose or use Decimal.
“In Python 3, a single slash is true division and always gives a float, so six divided by three is 2.0. Double slash is floor division: it rounds down, toward negative infinity, not toward zero. So seven floor-divided by two is 3, but minus seven floor-divided by two is minus 4. The modulo operator gives the remainder, and its sign follows the divisor, so minus seven modulo two is 1. That keeps floor division and the remainder consistent with each other. On floats, 0.1 can't be stored exactly in binary, so 0.1 plus 0.2 ends up a tiny bit off from 0.3 and the equality check fails. For comparisons I use math.isclose, and for money-like values where every digit matters I'd use the Decimal type.”
import math
6 / 3 # 2.0
7 // 2 # 3
-7 // 2 # -4
-7 % 2 # 1
0.1 + 0.2 == 0.3 # False
math.isclose(0.1 + 0.2, 0.3) # True
Saying // rounds toward zero, or suggesting floats are fine for exact values like account balances.
round(2.5) return in Python 3?What: a folder with its own interpreter link and its own installed packages, separate per project.
Create: python -m venv .venv, then activate it.
Install: pip install -r requirements.txt; record versions so others get the same setup.
“A virtual environment is a folder that gives one project its own set of installed packages. Without it, every project on my machine shares one global set, so two projects that need different versions of the same library end up fighting. I create one with python -m venv .venv in the project folder, then activate it: source .venv/bin/activate on macOS or Linux, or the activate script in the Scripts folder on Windows. After that, pip install puts packages inside that folder only. To share the setup, I list the packages in requirements.txt and teammates run pip install -r requirements.txt. I don't commit the .venv folder itself; it goes in .gitignore.”
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# after adding a package, record exact versions
pip freeze > requirements.txt
Installing everything globally with sudo pip, or committing the whole environment folder to Git.
nums[-1], nums[1:4] and text[::-1] give you?Negative index: counts from the end, so -1 is the last item.
Start and stop: start is included, stop is not, so 1:4 gives three items.
Step: the third number is the stride; -1 walks backwards and reverses the sequence.
Safety: a slice past the end just stops, while a single index past the end raises IndexError.
“Indexes start at zero, and negative indexes count from the end, so nums[-1] is the last item. A slice is start, stop and step. The start is included and the stop isn't, so nums[1:4] gives me the items at positions one, two and three. If I leave out start or stop, it means from the beginning or to the end. The step is how far to jump each time, and a step of minus one walks backwards, so text[::-1] reverses a string. Two things I keep in mind: slicing a list gives me a new list, and a slice that runs past the end just stops quietly, while a single index past the end raises an IndexError.”
nums = [10, 20, 30, 40, 50]
nums[-1] # 50
nums[1:4] # [20, 30, 40]
nums[::2] # [10, 30, 50]
"python"[::-1] # 'nohtyp'
nums[3:100] # [40, 50], no error
Thinking the stop index is included, or that slicing a list changes the original list.
copy = nums[:] copy the objects inside the list too?append, extend and insert on a list? What happens if you append one list to another?| append | adds exactly one object at the end, even if that object is a list. |
|---|---|
| extend | loops over an iterable and adds each item; += on a list does the same. |
| insert | puts one item at a given index and shifts the rest, which is slower on big lists. |
“append adds one object to the end. If that object is a list, I get a list inside my list, which surprises people the first time. extend takes any iterable and adds each of its items one by one, so extending with two numbers makes the list two longer. One gotcha is that extending with a string adds each character separately. insert takes an index and an item and puts it at that position, shifting everything after it along, so inserting at the front of a large list is slow compared with appending at the end. If I need to add at the front a lot, I'd use a deque from collections instead.”
a = [1, 2]
a.append([3, 4]) # [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4]) # [1, 2, 3, 4]
b.insert(0, 0) # [0, 1, 2, 3, 4]
Saying append and extend do the same thing, or not knowing that appending a list nests it.
nums += "ab" do to a list of numbers?nums.extend(other) and nums = nums + other?d[key] and d.get(key)? How would you count how many times each item appears in a list?Brackets: raise KeyError when the key is missing.
get: returns None, or a default you pass, when the key is missing.
Counting: counts.get(x, 0) + 1, or collections.Counter which does it for you.
“d[key] gives me the value but raises a KeyError if the key isn't there. d.get(key) returns None instead, or a default I pass as the second argument. I use brackets when a missing key would be a real bug and I want it to fail loudly, and get when missing is normal. For counting, the simple version loops over the items and does counts[item] = counts.get(item, 0) + 1. In real code I'd just use Counter from collections, which does the counting in one line and gives me most_common for free. defaultdict(int) is another option when I'm building counts inside a bigger loop.”
from collections import Counter
fruits = ["apple", "kiwi", "apple"]
counts = {}
for f in fruits:
counts[f] = counts.get(f, 0) + 1
# {'apple': 2, 'kiwi': 1}
Counter(fruits).most_common(1) # [('apple', 2)]
Wrapping every lookup in try/except without knowing get exists, or saying get raises an error for a missing key.
setdefault do?list.sort() and sorted()? And what ends up in nums after nums = nums.sort()?| list.sort() | sorts the list itself and returns None; only lists have it. |
|---|---|
| sorted() | takes any iterable and returns a new sorted list, leaving the original alone. |
Options: both take key and reverse, and both are stable, so equal items keep their order.
“list.sort() sorts the list itself and returns None. sorted() takes any iterable, like a list, a tuple, a string or a dict's keys, and gives me back a brand new list, leaving the original untouched. The classic bug is writing nums = nums.sort(). The sort happens, but then I overwrite nums with the None that sort returned, so my list is gone. I use sort() when I own the list and don't need the old order, and sorted() when I need to keep the original or I'm sorting something that isn't a list. Both take a key function and reverse=True. Both are also stable, meaning items that compare equal keep their original order, which is handy when I sort by one field and then another.”
nums = [3, 1, 2]
result = nums.sort()
print(result) # None
print(nums) # [1, 2, 3]
words = ("pear", "fig", "apple")
sorted(words, key=len) # ['fig', 'pear', 'apple']
Writing nums = nums.sort() and not seeing why the list became None, or thinking sorted() changes the original.
sorted() give you back when you pass it a dict?count += 1 on a global count raise UnboundLocalError? How would you fix it?The rule: any assignment to a name inside a function makes that name local for the whole function.
The error: count += 1 reads the local count before it has a value.
Lookup order: local, enclosing function, global, then built-ins.
The fix: pass the value in and return it; use global or nonlocal only when you mean it.
“Python decides at compile time which names in a function are local. If a function assigns to a name anywhere, even with +=, that name is local for the whole function. So count += 1 tries to read the local count before it has been given a value, and I get an UnboundLocalError. When a name isn't assigned in the function, Python looks it up in order: local, then any enclosing function, then the module's globals, then built-ins. To fix it, I'd usually pass the count in and return the new value, which keeps the function easy to test. If I really need to change the module-level variable, I declare global count. Inside a nested function, nonlocal does the same for the outer function's variable.”
count = 0
def bump():
count += 1 # UnboundLocalError
def bump_fixed(count):
return count + 1
count = bump_fixed(count)
Saying the variable 'doesn't exist yet' without explaining that the assignment makes it local, or reaching for global as the default fix.
nonlocal do that global can't?.append on it without any keyword?map and filter with one, and tell me when you'd use a comprehension instead.Lambda: a small unnamed function made of a single expression.
map and filter: apply a function to each item, or keep items where it returns true; both return lazy iterators.
Comprehension: usually clearer for simple transforms; lambdas shine as a key for sorting.
“A lambda is a small function with no name, written in one expression, like lambda x: x * 2. It can't hold statements or multiple lines. map applies a function to every item, and filter keeps only the items where the function returns something truthy. In Python 3 both give back lazy iterators, so I wrap them in list() if I need the whole result. Honestly, for simple cases I find a list comprehension easier to read, because the logic sits right there, with no lambda. Where lambdas really earn their place is as a key argument, like sorting a list of students by marks. If a lambda gets long or I need it twice, I give it a proper name with def.”
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda n: n * 2, nums))
evens = list(filter(lambda n: n % 2 == 0, nums))
evens_too = [n for n in nums if n % 2 == 0]
students = [{"name": "Ana", "marks": 81}, {"name": "Ben", "marks": 92}]
students.sort(key=lambda s: s["marks"], reverse=True)
Writing long, nested lambdas and calling it clean code, or not knowing that map returns an iterator in Python 3.
map(str, nums) not show the values?Clean: keep only letters and digits, lowercased.
Compare: check the cleaned text against its reverse.
Improve: two pointers from both ends avoid building a reversed copy.
“First I clean the input, because the spec says spaces and punctuation don't count. I keep only characters where isalnum() is true and lowercase them. Then a palindrome is just text that equals its own reverse, so I compare the cleaned version with a reversed slice. That's linear time and uses extra memory for the cleaned copy. If the interviewer wants less memory, I'd use two pointers, one at each end, skip anything that isn't a letter or digit, and compare as they move inward, stopping at the first mismatch. I'd test it with an empty string, a single character, a sentence like 'A man, a plan, a canal: Panama', and a near-miss.”
def is_palindrome(text):
cleaned = [ch.lower() for ch in text if ch.isalnum()]
return cleaned == cleaned[::-1]
is_palindrome("A man, a plan, a canal: Panama") # True
Forgetting to strip punctuation and spaces, or writing a manual loop to reverse a string without knowing slicing.
casefold() be better than lower() here? Why?"I love Python" becomes "Python love I". What about extra spaces?Split: split() with no argument splits on any run of whitespace and drops empty pieces.
Reverse: reverse the list of words with a slice or reversed().
Join: " ".join(...) puts the words back with single spaces, building the string once.
“I'd split the sentence into words, reverse the list and join it back with spaces. The detail that matters is calling split() with no argument. That splits on any run of spaces, tabs or newlines and ignores whitespace at the ends, so extra spaces don't turn into empty words. If I wrote split(" ") instead, two spaces in a row would give me an empty string in the list. For reversing I use a slice with a step of minus one, then " ".join to glue the words back with single spaces. Strings can't be changed in place, so building a list and joining once is the normal way, rather than adding strings together in a loop. The whole thing is linear in the length of the sentence.”
def reverse_words(sentence):
words = sentence.split()
return " ".join(words[::-1])
reverse_words(" I love Python ") # 'Python love I'
"a b".split(" ") # ['a', '', 'b']
Using split(" ") and returning empty words for repeated spaces, or not knowing what join does.
split, by walking the string yourself?swiss. Why is calling s.count() inside a loop a poor choice?Count: one pass to count every character with a dict or Counter.
Find: a second pass over the string returns the first character with count one.
Complexity: linear time; the naive s.count(ch) in a loop is quadratic.
“The simple approach calls s.count(ch) for every character, but that scans the whole string each time, so it's quadratic. Better is two passes. First I count every character with a Counter. Then I walk the string again in its original order and return the first character whose count is one. If none qualifies, I return None. Both passes are linear, so it's O(n) time, and the extra memory is the number of distinct characters, which is small for normal text. I'd check an empty string, a string where everything repeats, and one where the answer is the last character, and I'd ask whether upper and lower case should count as the same.”
from collections import Counter
def first_unique(s):
counts = Counter(s)
for ch in s:
if counts[ch] == 1:
return ch
return None
first_unique("swiss") # 'w'
Using a set to find unique characters and losing the original order, or not being able to say why the nested approach is slow.
list(set(items)) do the job?The problem: a set drops duplicates but doesn't keep the order items came in.
Loop version: keep a seen set and append each item the first time you meet it.
Short version: list(dict.fromkeys(items)), since dicts keep insertion order.
“Turning a list into a set does remove duplicates, but a set makes no promise about order, so the result can come back shuffled. To keep the order, I loop through the list with a seen set. For each item, if it's not in seen, I add it to the set and append it to the result. Checking a set is fast on average, so the whole thing is linear. There's also a neat one-liner, list(dict.fromkeys(items)), which works because dicts keep insertion order in modern Python. Both need the items to be hashable, so for a list of lists I'd convert each inner list to a tuple first.”
def dedupe(items):
seen = set()
result = []
for x in items:
if x not in seen:
seen.add(x)
result.append(x)
return result
dedupe([3, 1, 3, 2, 1]) # [3, 1, 2]
Checking if x not in result against a list and not noticing it makes the solution quadratic.
x in seen faster for a set than for a list?Clarify: second largest distinct value? What to return for fewer than two distinct numbers?
One pass: track the largest and second largest as you go.
Update rule: a new largest pushes the old largest down to second; skip values equal to the largest.
“First I'd ask two things: do we mean the second largest distinct value, and what should come back if there isn't one. I'll assume distinct and return None. Then one pass is enough. I keep first and second, both starting as None. For each number, if it's bigger than first, the old first becomes second and the new number becomes first. Otherwise, if it's not equal to first and bigger than second, it becomes the new second. That's O(n) time and constant memory, better than sorting's O(n log n). I'd test with duplicates of the max, a single item, a list that's all the same number, and negative numbers.”
def second_largest(nums):
first = second = None
for n in nums:
if first is None or n > first:
first, second = n, first
elif n != first and (second is None or n > second):
second = n
return second
second_largest([5, 5, 3, 4]) # 4
Starting both trackers at zero, which breaks on lists of negative numbers, or ignoring duplicates of the largest value.
first and second at zero a bug?Brute force: check every pair; quadratic time, no extra memory.
Better: walk once, storing each number's index in a dict.
Check first: before storing a number, look up target - n so one item isn't used twice.
“The first idea is two nested loops that try every pair. It works but it's quadratic, so it gets slow on big lists. The better way uses a dict from number to index. I walk the list once. For each number, I work out what I still need, which is the target minus the number. If that value is already in the dict, I've found my pair and return both indexes. If not, I store the current number with its index and move on. Checking before storing means I never pair a number with itself, and it still works when the same value appears twice. That's O(n) time, and I pay for it with O(n) extra memory for the dict.”
def two_sum(nums, target):
seen = {}
for i, n in enumerate(nums):
need = target - n
if need in seen:
return seen[need], i
seen[n] = i
return None
two_sum([2, 7, 11, 15], 9) # (0, 1)
Storing the number before checking, which pairs an item with itself, or not being able to say what the dict costs in memory.
self and __init__ for? And what goes wrong if a class keeps a list as a class attribute?self: the instance the method was called on, passed in automatically as the first argument.
__init__: sets up a new object's starting data right after the object is created.
Class attribute: one value shared by every instance; a shared list collects everyone's items.
Fix: create the list inside __init__ so each object gets its own.
“self is the object the method was called on. When I write cart.add(item), Python passes cart in as self for me. __init__ runs right after a new object is created and sets its starting data, like self.owner = owner. The trap is putting a mutable value on the class itself. If I write items = [] in the class body, there's only one list, owned by the class, and every instance that calls self.items.append adds to that same list. So adding a pen to one cart makes it show up in every cart. The fix is to create the list inside __init__, so each object gets its own. Class attributes are fine for constants that everyone should share.”
class Cart:
items = [] # one list shared by every Cart
def add(self, item):
self.items.append(item)
a, b = Cart(), Cart()
a.add("pen")
print(b.items) # ['pen']
class FixedCart:
def __init__(self):
self.items = [] # each cart gets its own list
Calling self a keyword, or saying class attributes are copied into each instance.
self.items = [] instead of appending?__init__ and __new__?private keyword. What do a single leading underscore and a double leading underscore on an attribute mean?Single underscore: a convention meaning 'internal, don't rely on this'; nothing stops access.
Double underscore: triggers name mangling to _ClassName__attr, mainly to avoid clashes in subclasses.
Dunder names: names with underscores on both sides are special methods, not private ones.
Controlled access: use @property when you want checks around reading or setting a value.
“Python relies on convention rather than enforcement. A single leading underscore, like _balance, tells other developers it's internal and might change, but anyone can still read it. It also keeps the name out of from module import * when the module doesn't define __all__. A double leading underscore, like __balance, makes Python rename it inside the class to _Account__balance. That's called name mangling. Its real purpose is to stop a subclass from accidentally overwriting the parent's attribute, not to hide data, because you can still reach the mangled name. Names with double underscores on both sides, like __init__, are special methods and aren't mangled. When I need rules around a value, I use a @property.”
Claiming a double underscore makes an attribute truly private or impossible to access.
@property to stop a balance being set to a negative number?r, w and a do?Open safely: use with open(...) so the file closes even on an error, and pass an encoding.
Line by line: loop over the file object itself; it reads lazily.
Modes: r reads and fails if missing, w creates or empties the file, a creates or adds to the end.
“I open the file in a with block so it gets closed even if something goes wrong, and I pass encoding="utf-8" because the default depends on the machine. Then I loop over the file object directly. That reads one line at a time, so it's fine even for a big file, unlike read() or readlines(), which pull everything into memory. Each line keeps its newline, so I usually strip it. For modes: r is read, the default, and it raises FileNotFoundError if the file isn't there. w is write, which creates the file or empties an existing one, so it's easy to wipe data by mistake. a is append, which creates the file if needed and adds to the end.”
with open("marks.txt", "r", encoding="utf-8") as f:
for line in f:
name, score = line.rstrip("\n").split(",")
print(name, int(score))
with open("log.txt", "a", encoding="utf-8") as log:
log.write("done\n")
Using w to add to a log file and wiping it, or reading a huge file with read() in one go.
Exception?Define: a small class that inherits from a fitting built-in, such as ValueError.
Raise: with a clear message that includes the bad value.
Why: callers can catch exactly this error without swallowing unrelated ones.
“I'd define a small class, say InvalidAgeError, that inherits from ValueError, because bad input is a value problem. The body can just be pass. In my function, when the input is wrong, I raise InvalidAgeError with a message that says what was expected and what I got. The reason not to raise a plain Exception is that the caller can only catch it with except Exception, which also catches every other bug, like a typo that causes a NameError. With a specific class, the caller catches exactly that case and lets real bugs surface. Inheriting from ValueError also means code that already handles ValueError keeps working. If I'm turning one error into another, I use raise ... from err to keep the original cause.”
class InvalidAgeError(ValueError):
pass
def set_age(age):
if age < 0:
raise InvalidAgeError(f"age must not be negative, got {age}")
return age
try:
set_age(-4)
except InvalidAgeError as err:
print("Please fix:", err)
Raising bare Exception everywhere, or returning error strings like 'invalid' instead of raising.
raise NewError(...) from err add to the traceback?What it did: one line on the problem and who used it.
Your part: the pieces you wrote yourself, and one decision you made.
A problem: something that broke and how you fixed it.
Hindsight: one concrete thing you'd change now.
“In my final year, three of us built a tool that took exam marks from spreadsheets exported as CSV files, cleaned them and produced a report card for each student. My part was the reading and checking code. I wrote functions that parsed each row, flagged missing marks and caught names typed in different ways. Halfway through, one department's file used a different date format and the whole run crashed. I fixed it by validating each row and collecting the bad rows into an error file instead of stopping. I also added a few pytest tests after that scare. If I did it again, I'd use Git from day one, since we lost an afternoon merging copies by hand, and I'd write the tests before the code, not after.”
Describing the team's project in 'we' terms only, so the interviewer can't tell what you personally wrote.
The bug: what you saw, in one sentence.
Your method: how you narrowed it down step by step.
The cause: what it turned out to be.
The habit: what you do differently since.
“In a data structures lab, my recursive function to search a binary tree kept returning None, even for values I could see were in the tree. I spent an hour changing things at random, which didn't help. Then I slowed down. I made the smallest tree that failed, just three nodes, and used breakpoint() to step through each call. I saw the function found the value deep down, but the result vanished on the way back up. I'd written the recursive call without return in front of it, so the found value was thrown away. After that, I always start with the smallest input that fails and step through it before changing code, and I set myself a time limit before asking a classmate or the lab assistant.”
A story with no method, just 'I kept trying until it worked', or one where someone else found the bug.
Run it: set up the environment and get the project and its tests running locally.
Reproduce: make the bug happen and read the traceback from the bottom up.
Locate: search the code for the error message or function name; read only what you need.
Fix safely: add a test that fails, make the smallest change, run the tests, then ask for review.
“First I'd get the project running on my machine: create a virtual environment, install the requirements and run the existing tests, so I know what normal looks like. Then I'd reproduce the bug myself. If there's a traceback, I read it from the bottom up to find the exact line. I'd search the codebase for that function or the error message instead of trying to understand everything at once. Before changing anything, I'd write a small test that fails because of the bug. Then I make the smallest fix that makes it pass and run the full test suite. If I'm still stuck after an hour or so, I ask a teammate with what I've tried, not just 'it doesn't work'.”
Planning to read the whole codebase first, or rewriting a large chunk to fix a small bug.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.