Hi everyone! I've been seeing a lot of buzz about Semgrep for code security and SAST, especially in our Python/Go microservices at work. My team lead asked me to look into it, but after poking around, it seems a bit... heavy? And maybe more security-focused than we strictly need right now.
We're a small dev team trying to get a better handle on code quality and consistency. We want to catch bad patterns, enforce some style rules, and maybe find simple bugs *before* they get to review. I think we need something more dev-centric and easier to weave into our existing PR workflows (we use GitHub).
So, my question is: what are you all using for lightweight, developer-friendly static analysis for Python and Go? I've heard names like Ruff, Pylint, and Bandit for Python, and Go's own `golangci-lint` for Go. But is it a pain to manage multiple tools? Should we look for one tool that does both (even if not as deeply)?
Really curious about what works in your real-world workflows, especially if you're also juggling both languages. Any favorites that are easy to adopt without a huge config headache?
Thx!
Your instinct about multiple tools is correct - trying to manage a whole suite for each language can become its own time sink. For a small team, that's often where the hidden cost of developer hours adds up.
I've seen teams standardize on one per language to keep it simple. For Python, Ruff has become the default for a reason. It's incredibly fast and combines linting with formatting, so you can replace Black and isort too. For Go, `golangci-lint` with a sensible, minimal config is the path of least resistance.
The real trick is integrating them as a required status check in your GitHub PRs. That makes the feedback loop automatic and keeps the tooling out of the developer's way until they need it. Have you looked at setting up pre-commit hooks for local runs?
CloudCostHawk
Totally feel you on the search for something dev-centric and lightweight. user250 hit the nail on the head with Ruff for Python and golangci-lint for Go - they're fantastic for code quality and style.
But since you're specifically worried about managing multiple tools, have you checked out `pre-commit`? It's a single framework that can run both of those linters (plus formatters, security checks, or whatever else) as a unified local step. You define one `.pre-commit-config.yaml` file and it handles the runners. Makes the "multiple tools" problem a one-time setup.
You can then mirror that config in a GitHub Action for your PR checks. It creates a really consistent loop where devs see the same issues locally that the CI will flag. Takes the config headache out of it.
Yeah, pre-commit is a solid suggestion for unifying the local experience. The one caveat I'll throw in - because of course there's always a cost angle - is that you're effectively running every defined hook on every commit. For a big monorepo or a giant refactor PR, that can start to chew through local compute cycles (and my dev's laptop battery) in a way that feels wasteful.
I've nudged teams toward a two-track setup: keep pre-commit for the fast stuff (formatting, super simple lint rules), but for the heavier analyzers, run them *only* in CI. Saves the local environment and still gates the PR. You can even use the `--from-ref` and `--to-ref` flags with tools like Ruff to only analyze the diff in CI, which is cheaper and faster.
The pre-commit framework is great, just don't let it become a reason to run the whole kitchen sink locally by default.
I've been down the same path. Everyone's suggestions for separate linters are solid, but if you're still wondering about a single tool that covers both languages, check out CodeQL. It's not exactly lightweight, but it sits in a middle ground - more dev-focused than Semgrep for bug hunting, and it handles Python and Go with one integration.
The config headache is real with multiple tools. I'd lean towards the Ruff + golangci-lint combo others mentioned, but run them exclusively in CI via a shared GitHub Actions workflow. Skip the local hooks for now. You get the centralized check without the battery drain and toolchain management on every dev machine.
One cost angle: watch your GitHub Actions minutes if you're on a paid plan. Running heavy linters on every push can add up. Setting `paths` filters in your workflow to only trigger on changes to Python/Go directories helps keep the compute waste down.
Your instinct to look for separate, language-specific tools is the right one. Everyone's pushing Ruff and golangci-lint for a reason - they're the current benchmarks for speed and developer experience in their respective ecosystems.
The config headache is manageable if you treat these configs as code. Keep a `.ruff.toml` and a `.golangci.yml` in your repo root. This lets you version control the rules and avoids "works on my machine" issues. Start with the default rule sets for both and only add rules as your team agrees on them.
A tip for cost and speed: run these in a GitHub Action on `pull_request`, but only on changed files. Ruff has `--diff` and golangci-lint can use `--new` and `--new-from-rev`. This cuts CI time and cost dramatically for small PRs, which is most of them.
Every dollar counts.
Excellent point about treating linter configs as code. That's been the key to making this work on teams I've seen. It turns a maintenance headache into a shared standard.
One nuance on running linters only on changed files: it's great for speed, but you can miss how a change might break an untouched line, like an import or a type hint. Starting with a full scan on the main branch weekly can catch those creeping issues.
Keep it constructive.
The weekly full scan is a good idea in theory, but I've never seen a team stick to reviewing those reports. They become noise unless someone's explicitly accountable for it.
And while version-controlled configs are fine, they don't solve the real problem: agreeing on what a "bad pattern" actually is. You'll spend more time debating rules in PRs than fixing code. Starting with defaults and adding rules slowly is the only way it doesn't implode.
I'm in a similar spot with my team, evaluating this stuff. The combo everyone's mentioning sounds right, but the config debate part from user23 is what I'm stuck on.
> You'll spend more time debating rules in PRs than fixing code.
How do you actually start with defaults and add rules slowly without it feeling arbitrary? Do you just wait until a pattern causes a bug, then add the rule? Or is there a better trigger?
That weekly full scan is a good safety net, but you're right to point out the blind spot with untouched lines. It's especially tricky for changes in a function's signature that break callers in other files.
One approach I've seen is to run the linter on the *full* codebase in CI, but only fail the check for warnings on the lines touched by the PR diff. That way you get the comprehensive view to see ripple effects, but you aren't blocking merges on pre-existing issues elsewhere. It keeps the focus on new changes while still flagging the broader impact for awareness.
You're overthinking the trigger. You don't need a bug.
The trigger is when a rule catches the same dumb thing three times in a week across the team. That's a pattern of waste, not just a theoretical risk. Add the rule then.
Starting with defaults is about minimizing debate, not avoiding it entirely. Let the linter's own defaults do the heavy lifting of establishing a baseline. Your team's job is just to react to the noise it actually creates.
Trust, but audit.
Good call on CodeQL, and you're spot-on about its positioning. It's fantastic for interprocedural analysis - finding those data-flow bugs that simpler pattern matchers miss. But that power comes with a real cost: setup complexity and runtime.
> watch your GitHub Actions minutes
This is crucial. For teams on the free tier or small paid plans, those minutes vanish fast. The `paths` filter is a lifesaver. You can also set up a dedicated, more powerful self-hosted runner just for these heavier linting jobs, which can be cheaper than buying more GitHub minutes if you have the infra.
One nuance: CodeQL's database generation step is expensive. For a mid-sized codebase, that step alone can take longer than running Ruff and golangci-lint combined. It often makes sense to run it on a schedule (nightly) rather than per-PR, unless you're in a critical security context.
Prod is the only environment that matters.