O'Donnell began his career as a youth player with Wishaw Wycombe Wanderers and captained the Aberdeen under-17 team. He then joined Celtic, where he was part of the team that won the Scottish Youth Cup and under-19 league title in 2010–11.
Partick Thistle
O'Donnell was released by Celtic in 2011 and on 23 August 2011 signed for Partick Thistle on a one-year contract after impressing on trial. He went on to make his debut on 17 September as a substitute in a 4–0 win at home to Ayr United. In 2012–13, O'Donnell played in 38 matches as Partick Thistle won promotion to the Scottish Premiership and reached the final of the Scottish Challenge Cup. He was named in the PFA Scotland First Division Team of the Year as a result of his performances. On 27 January 2015, Championship club Blackpool upped an initial offer of £50,000 for O'Donnell to £125,000. This offer was accepted by Partick Thistle, however, O'Donnell rejected the personal terms on offer at Blackpool in favour of seeing the remaining six months of his contract. He left Partick Thistle upon the expiry of his contract at the end of the 2014–15 season, having made 139 appearances for the club.
Luton Town
On 22 June 2015, O'Donnell signed a two-year contract with League Two club Luton Town. He made his Luton debut in a 3–1 win at home to newly promoted Championship club Bristol City in the League Cup on 11 August. O'Donnell's first goal for the club was a 90th-minute winner in a 2–1 win at home to Leyton Orient in the Football League Trophy on 1 September. He finished the 2015–16 season with 33 appearances and one goal. O'Donnell netted his second Luton goal in 2016–17 from a 25-yard shot in a 1–1 draw at home to Yeovil Town on 13 August 2016. However, a loss of form saw O'Donnell lose his place in the team to academy graduate James Justin after a 2–0 defeat away to Crawley Town on 17 September. He regained his place in the team to make his first league start in five matches after Justin suffered a calf injury in the warm up before Luton's 2–1 win away to Leyton Orient on 15 October. O'Donnell lost his place once more when Justin returned from injury for Luton's 3–1 win away to Exeter City in the FA Cup on 5 November, but a half-time substitute appearance in the next round at home to National League club Solihull Moors on 3 December saw him score twice as Luton overturned a 2–0 deficit to win 6–2. He was then recalled to the starting lineup by manager Nathan Jones for the subsequent league match at home to Carlisle United, which resulted in a 1–1 draw. O'Donnell was released when his contract expired at the end of 2016–17.
Kilmarnock
O'Donnell signed a three-year contract with Scottish Premiership club Kilmarnock on 4 July 2017. O'Donnell performed well following the appointment of Steve Clarke as Kilmarnock manager, and he was selected for the Scotland squad in May 2018. Kilmarnock offered O'Donnell a new three-year contract in August 2019, but he opted to evaluate his contract situation at the end of the season. During January 2020 Oxford United made an offer for O'Donnell, but a deal was not completed before the closure of the transfer window. He rejected a new contract with Kilmarnock at the end of the 2019–20 season.
International career
O'Donnell made one appearance for the Scotland under-21 team in February 2013. He received his first call-up to the Scotland national team in May 2018, for friendly matches against Peru and Mexico. O'Donnell made his full Scotland debut on 29 May 2018, in a 2–0 defeat to Peru. He retained his place in the squad for the matches in September 2018, and "excelled" in the 2–0 win against Albania on 10 September.